From e72eea0081d727eddfec28d8bba4c9042ed316a0 Mon Sep 17 00:00:00 2001 From: Yang Mingshan Date: Sun, 8 Mar 2026 02:44:53 +0800 Subject: [PATCH 1/7] fix(scheduler): align post-flush and flush --- .../runtime-core/__tests__/scheduler.spec.ts | 77 +++++++++++++++++++ packages/runtime-core/src/scheduler.ts | 39 ++++++---- 2 files changed, 101 insertions(+), 15 deletions(-) diff --git a/packages/runtime-core/__tests__/scheduler.spec.ts b/packages/runtime-core/__tests__/scheduler.spec.ts index 90b27eaf4e3..668056114bd 100644 --- a/packages/runtime-core/__tests__/scheduler.spec.ts +++ b/packages/runtime-core/__tests__/scheduler.spec.ts @@ -537,6 +537,58 @@ describe('scheduler', () => { expect(job2).toHaveBeenCalledTimes(1) }) + test('post jobs can be re-queued after an error', async () => { + const err = new Error('test') + let shouldThrow = true + + const job1: SchedulerJob = vi.fn(() => { + if (shouldThrow) { + shouldThrow = false + throw err + } + }) + const job2: SchedulerJob = vi.fn() + + queuePostFlushCb(job1, 1) + queuePostFlushCb(job2, 2) + + try { + await nextTick() + } catch (e: any) { + expect(e).toBe(err) + } + + expect(job1).toHaveBeenCalledTimes(1) + expect(job2).toHaveBeenCalledTimes(0) + + queuePostFlushCb(job1, 1) + queuePostFlushCb(job2, 2) + + await nextTick() + + expect(job1).toHaveBeenCalledTimes(2) + expect(job2).toHaveBeenCalledTimes(1) + }) + + test(`jobs won't be left on queue after an post job error`, async () => { + const job1: SchedulerJob = vi.fn(() => { + queueJob(job2, 2) + queuePostFlushCb(job3, 1) + throw new Error('test') + }) + const job2: SchedulerJob = vi.fn() + const job3: SchedulerJob = vi.fn() + + queuePostFlushCb(job1, 1) + + try { + await nextTick() + } catch {} + + expect(job2.flags! & SchedulerJobFlags.QUEUED).toBeFalsy() + expect(job3.flags! & SchedulerJobFlags.QUEUED).toBeFalsy() + }) + test('should prevent self-triggering jobs by default', async () => { let count = 0 const job = () => { @@ -634,6 +686,31 @@ describe('scheduler', () => { expect(job2).toHaveBeenCalledTimes(2) }) + test(`recursive post jobs can't be re-queued by other jobs`, async () => { + let recurse = true + + const job1: SchedulerJob = () => { + if (recurse) { + // job2 is already queued, so this shouldn't do anything + queuePostFlushCb(job2, 2) + recurse = false + } + } + const job2: SchedulerJob = vi.fn(() => { + if (recurse) { + queuePostFlushCb(job1, 1) + queuePostFlushCb(job2, 2) + } + }) + job2.flags = SchedulerJobFlags.ALLOW_RECURSE + + queuePostFlushCb(job2, 2) + + await nextTick() + + expect(job2).toHaveBeenCalledTimes(2) + }) + test('jobs are de-duplicated correctly when calling flushPreFlushCbs', async () => { let recurse = true diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index cf2abb920d2..89139e0e054 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -218,25 +218,34 @@ export function flushPostFlushCbs(seen?: CountMap): void { seen = seen || new Map() } - while (postFlushIndex < activePostJobs.length) { - const cb = activePostJobs[postFlushIndex++] - if (__DEV__ && checkRecursiveUpdates(seen!, cb)) { - continue - } - if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) { - cb.flags! &= ~SchedulerJobFlags.QUEUED - } - if (!(cb.flags! & SchedulerJobFlags.DISPOSED)) { - try { - cb() - } finally { + try { + while (postFlushIndex < activePostJobs.length) { + const cb = activePostJobs[postFlushIndex++] + if (__DEV__ && checkRecursiveUpdates(seen!, cb)) { + continue + } + if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) { cb.flags! &= ~SchedulerJobFlags.QUEUED } + if (!(cb.flags! & SchedulerJobFlags.DISPOSED)) { + try { + cb() + } finally { + if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) { + cb.flags! &= ~SchedulerJobFlags.QUEUED + } + } + } + } + } finally { + // If there was an error we still need to clear the QUEUED flags + while (postFlushIndex < activePostJobs.length) { + activePostJobs[postFlushIndex++].flags! &= ~SchedulerJobFlags.QUEUED } - } - activePostJobs = null - postFlushIndex = 0 + activePostJobs = null + postFlushIndex = 0 + } } } From 2545a6c7ce1fdf1a77480776b8f4f841fb37285c Mon Sep 17 00:00:00 2001 From: Yang Mingshan Date: Sun, 8 Mar 2026 03:07:03 +0800 Subject: [PATCH 2/7] chore: typo --- packages/runtime-core/__tests__/scheduler.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-core/__tests__/scheduler.spec.ts b/packages/runtime-core/__tests__/scheduler.spec.ts index 668056114bd..bc91ed122bb 100644 --- a/packages/runtime-core/__tests__/scheduler.spec.ts +++ b/packages/runtime-core/__tests__/scheduler.spec.ts @@ -570,7 +570,7 @@ describe('scheduler', () => { expect(job2).toHaveBeenCalledTimes(1) }) - test(`jobs won't be left on queue after an post job error`, async () => { + test(`jobs won't be left on queue after a post job error`, async () => { const job1: SchedulerJob = vi.fn(() => { queueJob(job2, 2) queuePostFlushCb(job3, 1) From 4999f7faff60f89c6e8b185abd34725124fde65b Mon Sep 17 00:00:00 2001 From: Yang Mingshan Date: Sun, 8 Mar 2026 14:13:15 +0800 Subject: [PATCH 3/7] fix(scheduler): reset currentFlushPromise only when no jobs are left to flush --- packages/runtime-core/src/scheduler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index 89139e0e054..89a809128b0 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -306,10 +306,11 @@ function flushJobs(seen?: CountMap) { flushPostFlushCbs(seen) - currentFlushPromise = null // If new jobs have been added to either queue, keep flushing if (jobsLength || postJobs.length) { flushJobs(seen) + } else { + currentFlushPromise = null } } } From f5dbdfd26d954ef3a15449f4d3cf30a00d279ec1 Mon Sep 17 00:00:00 2001 From: Yang Mingshan Date: Sun, 8 Mar 2026 15:05:55 +0800 Subject: [PATCH 4/7] fix(scheduler): keep flush after post job error --- packages/runtime-core/src/scheduler.ts | 27 ++++++++++---------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index 89a809128b0..adc0675ad19 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -135,18 +135,9 @@ function queueJobWorker( return false } -const doFlushJobs = () => { - try { - flushJobs() - } catch (e) { - currentFlushPromise = null - throw e - } -} - function queueFlush() { if (!currentFlushPromise) { - currentFlushPromise = resolvedPromise.then(doFlushJobs) + currentFlushPromise = resolvedPromise.then(flushJobs) } } @@ -304,13 +295,15 @@ function flushJobs(seen?: CountMap) { flushIndex = 0 jobsLength = 0 - flushPostFlushCbs(seen) - - // If new jobs have been added to either queue, keep flushing - if (jobsLength || postJobs.length) { - flushJobs(seen) - } else { - currentFlushPromise = null + try { + flushPostFlushCbs(seen) + } finally { + // If new jobs have been added to either queue, keep flushing + if (jobsLength || postJobs.length) { + flushJobs(seen) + } else { + currentFlushPromise = null + } } } } From d3668b7b79a2040f9275f0f457d2d053a1b9be10 Mon Sep 17 00:00:00 2001 From: Yang Mingshan Date: Sun, 8 Mar 2026 15:57:59 +0800 Subject: [PATCH 5/7] fix(scheduler): improve error recovery --- .../runtime-core/__tests__/scheduler.spec.ts | 62 +++++++++++++++++++ packages/runtime-core/src/scheduler.ts | 18 ++++-- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/packages/runtime-core/__tests__/scheduler.spec.ts b/packages/runtime-core/__tests__/scheduler.spec.ts index bc91ed122bb..d7d1618b86d 100644 --- a/packages/runtime-core/__tests__/scheduler.spec.ts +++ b/packages/runtime-core/__tests__/scheduler.spec.ts @@ -1,6 +1,7 @@ import { type SchedulerJob, SchedulerJobFlags, + flushOnAppMount, flushPostFlushCbs, flushPreFlushCbs, nextTick, @@ -501,6 +502,67 @@ describe('scheduler', () => { await nextTick() }) + test('flushOnAppMount error recovery', () => { + const err = new Error('test') + let shouldThrow = true + + const job1: SchedulerJob = vi.fn(() => { + if (shouldThrow) { + shouldThrow = false + throw err + } + }) + + queuePostFlushCb(job1) + + try { + flushOnAppMount() + } catch (e: any) { + expect(e).toBe(err) + } + + expect(job1).toHaveBeenCalledTimes(1) + + queuePostFlushCb(job1) + + flushOnAppMount() + + expect(job1).toHaveBeenCalledTimes(2) + }) + + test('pre jobs can be re-queued after an error', () => { + const err = new Error('test') + let shouldThrow = true + + const job1: SchedulerJob = vi.fn(() => { + if (shouldThrow) { + shouldThrow = false + throw err + } + }) + const job2: SchedulerJob = vi.fn() + + queueJob(job1, undefined, true) + queueJob(job2, undefined, true) + + try { + flushPreFlushCbs() + } catch (e: any) { + expect(e).toBe(err) + } + + expect(job1).toHaveBeenCalledTimes(1) + expect(job2).toHaveBeenCalledTimes(0) + + queueJob(job1, undefined, true) + queueJob(job2, undefined, true) + + flushPreFlushCbs() + + expect(job1).toHaveBeenCalledTimes(2) + expect(job2).toHaveBeenCalledTimes(1) + }) + test('jobs can be re-queued after an error', async () => { const err = new Error('test') let shouldThrow = true diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index adc0675ad19..d55f72f6b9c 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -186,9 +186,12 @@ export function flushPreFlushCbs( if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) { cb.flags! &= ~SchedulerJobFlags.QUEUED } - cb() - if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) { - cb.flags! &= ~SchedulerJobFlags.QUEUED + try { + cb() + } finally { + if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) { + cb.flags! &= ~SchedulerJobFlags.QUEUED + } } } } @@ -247,9 +250,12 @@ let isFlushing = false export function flushOnAppMount(instance?: GenericComponentInstance): void { if (!isFlushing) { isFlushing = true - flushPreFlushCbs(instance) - flushPostFlushCbs() - isFlushing = false + try { + flushPreFlushCbs(instance) + flushPostFlushCbs() + } finally { + isFlushing = false + } } } From 5f62769660c15e4d9cce6dd2c220ea110df94277 Mon Sep 17 00:00:00 2001 From: Yang Mingshan Date: Sun, 8 Mar 2026 16:13:57 +0800 Subject: [PATCH 6/7] Revert "fix(scheduler): keep flush after post job error" --- .../runtime-core/__tests__/scheduler.spec.ts | 19 ------------- packages/runtime-core/src/scheduler.ts | 27 ++++++++++++------- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/packages/runtime-core/__tests__/scheduler.spec.ts b/packages/runtime-core/__tests__/scheduler.spec.ts index d7d1618b86d..75f81d96d80 100644 --- a/packages/runtime-core/__tests__/scheduler.spec.ts +++ b/packages/runtime-core/__tests__/scheduler.spec.ts @@ -632,25 +632,6 @@ describe('scheduler', () => { expect(job2).toHaveBeenCalledTimes(1) }) - test(`jobs won't be left on queue after a post job error`, async () => { - const job1: SchedulerJob = vi.fn(() => { - queueJob(job2, 2) - queuePostFlushCb(job3, 1) - throw new Error('test') - }) - const job2: SchedulerJob = vi.fn() - const job3: SchedulerJob = vi.fn() - - queuePostFlushCb(job1, 1) - - try { - await nextTick() - } catch {} - - expect(job2.flags! & SchedulerJobFlags.QUEUED).toBeFalsy() - expect(job3.flags! & SchedulerJobFlags.QUEUED).toBeFalsy() - }) - test('should prevent self-triggering jobs by default', async () => { let count = 0 const job = () => { diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index d55f72f6b9c..87d4905b999 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -135,9 +135,18 @@ function queueJobWorker( return false } +const doFlushJobs = () => { + try { + flushJobs() + } catch (e) { + currentFlushPromise = null + throw e + } +} + function queueFlush() { if (!currentFlushPromise) { - currentFlushPromise = resolvedPromise.then(flushJobs) + currentFlushPromise = resolvedPromise.then(doFlushJobs) } } @@ -301,15 +310,13 @@ function flushJobs(seen?: CountMap) { flushIndex = 0 jobsLength = 0 - try { - flushPostFlushCbs(seen) - } finally { - // If new jobs have been added to either queue, keep flushing - if (jobsLength || postJobs.length) { - flushJobs(seen) - } else { - currentFlushPromise = null - } + flushPostFlushCbs(seen) + + // If new jobs have been added to either queue, keep flushing + if (jobsLength || postJobs.length) { + flushJobs(seen) + } else { + currentFlushPromise = null } } } From 87742a8dd578f04843281563589500e36c2cd45b Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 9 Mar 2026 08:45:27 +0800 Subject: [PATCH 7/7] fix(scheduler): ensure leftover jobs are queued after flush errors --- .../runtime-core/__tests__/scheduler.spec.ts | 19 +++++++++++++++++++ packages/runtime-core/src/scheduler.ts | 5 +++++ 2 files changed, 24 insertions(+) diff --git a/packages/runtime-core/__tests__/scheduler.spec.ts b/packages/runtime-core/__tests__/scheduler.spec.ts index 75f81d96d80..3028985ce09 100644 --- a/packages/runtime-core/__tests__/scheduler.spec.ts +++ b/packages/runtime-core/__tests__/scheduler.spec.ts @@ -632,6 +632,25 @@ describe('scheduler', () => { expect(job2).toHaveBeenCalledTimes(1) }) + test('post job error should not leave newly queued main jobs pending', async () => { + const calls: string[] = [] + + const job2: SchedulerJob = () => { + calls.push('job2') + } + + const job1: SchedulerJob = () => { + queueJob(job2, 2) + throw new Error('test') + } + + queuePostFlushCb(job1, 1) + + await expect(nextTick()).rejects.toThrow('test') + await nextTick() + expect(calls).toEqual(['job2']) + }) + test('should prevent self-triggering jobs by default', async () => { let count = 0 const job = () => { diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index 87d4905b999..e1c04a58584 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -140,6 +140,11 @@ const doFlushJobs = () => { flushJobs() } catch (e) { currentFlushPromise = null + // If a nested pre/post flush throws after queueing more work, defer the + // leftovers to a fresh microtask + if (jobsLength || postJobs.length) { + queueFlush() + } throw e } }