Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions packages/runtime-core/__tests__/scheduler.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
type SchedulerJob,
SchedulerJobFlags,
flushOnAppMount,
flushPostFlushCbs,
flushPreFlushCbs,
nextTick,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -537,6 +599,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('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 = () => {
Expand Down Expand Up @@ -634,6 +748,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

Expand Down
65 changes: 43 additions & 22 deletions packages/runtime-core/src/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -195,9 +200,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
}
}
}
}
Expand All @@ -218,25 +226,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
}
}
}

Expand All @@ -247,9 +264,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
}
}
}

Expand Down Expand Up @@ -297,10 +317,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
}
}
}
Expand Down
Loading