fix: improve abortController robustness with defensive programming (#7856)

This commit is contained in:
Jason Young 2025-07-06 14:18:03 +08:00 committed by GitHub
parent a1304054ce
commit 60a3cac80d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 221 additions and 3 deletions

View File

@ -0,0 +1,213 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
abortCompletion,
abortMap,
addAbortController,
createAbortPromise,
removeAbortController
} from '../abortController'
// Mock logger
vi.mock('@renderer/config/logger', () => ({
default: {
log: vi.fn()
}
}))
describe('abortController', () => {
beforeEach(() => {
// 清理全局 Map
abortMap.clear()
})
describe('addAbortController', () => {
it('should add abort function to map', () => {
const abortFn = vi.fn()
addAbortController('test-id', abortFn)
expect(abortMap.get('test-id')).toContain(abortFn)
})
it('should handle multiple abort functions for same id', () => {
const fn1 = vi.fn()
const fn2 = vi.fn()
addAbortController('test-id', fn1)
addAbortController('test-id', fn2)
const fns = abortMap.get('test-id')
expect(fns).toHaveLength(2)
expect(fns).toEqual([fn1, fn2])
})
it('should handle duplicate functions for same id', () => {
// 测试重复添加相同函数
const fn = vi.fn()
addAbortController('test-id', fn)
addAbortController('test-id', fn)
const fns = abortMap.get('test-id')
expect(fns).toHaveLength(2)
expect(fns).toEqual([fn, fn])
})
it('should handle empty string id', () => {
// 测试空字符串 id
const fn = vi.fn()
addAbortController('', fn)
expect(abortMap.get('')).toContain(fn)
})
})
describe('removeAbortController', () => {
it('should remove specific abort function', () => {
const fn1 = vi.fn()
const fn2 = vi.fn()
addAbortController('test-id', fn1)
addAbortController('test-id', fn2)
removeAbortController('test-id', fn1)
expect(abortMap.get('test-id')).toEqual([fn2])
})
it('should handle non-existent function gracefully', () => {
const fn1 = vi.fn()
const fn2 = vi.fn()
addAbortController('test-id', fn1)
// 删除不存在的函数,原函数应该保持
removeAbortController('test-id', fn2)
expect(abortMap.get('test-id')).toEqual([fn1])
})
it('should handle empty string id', () => {
// 测试空字符串 id
const fn = vi.fn()
addAbortController('', fn)
removeAbortController('', fn)
expect(abortMap.get('')).toEqual([])
})
it('should handle non-existent id gracefully', () => {
// 测试不存在的 id
const fn = vi.fn()
expect(() => removeAbortController('non-existent-id', fn)).not.toThrow()
})
})
describe('abortCompletion', () => {
it('should call all abort functions and clean up', () => {
const fn1 = vi.fn()
const fn2 = vi.fn()
addAbortController('test-id', fn1)
addAbortController('test-id', fn2)
abortCompletion('test-id')
// 验证所有函数被调用
expect(fn1).toHaveBeenCalledTimes(1)
expect(fn2).toHaveBeenCalledTimes(1)
// 验证清理完成 - 数组变为空但条目仍存在
expect(abortMap.get('test-id')).toEqual([])
})
it('should handle non-existent id gracefully', () => {
expect(() => abortCompletion('non-existent')).not.toThrow()
})
it('should handle empty string id', () => {
// 测试空字符串 id
expect(() => abortCompletion('')).not.toThrow()
})
it('should handle empty function array', () => {
// 测试空函数数组
abortMap.set('test-id', [])
expect(() => abortCompletion('test-id')).not.toThrow()
expect(abortMap.has('test-id')).toBe(true) // 空数组不会被处理
})
})
describe('createAbortPromise', () => {
it('should reject immediately if signal already aborted', async () => {
const controller = new AbortController()
controller.abort()
const promise = createAbortPromise(controller.signal, Promise.resolve('success'))
await expect(promise).rejects.toMatchObject({
name: 'AbortError',
message: 'Operation aborted'
})
})
it('should reject when signal is aborted later', async () => {
const controller = new AbortController()
const finallyPromise = new Promise<string>(() => {}) // 永不解析的 Promise
const promise = createAbortPromise(controller.signal, finallyPromise)
// 稍后中止
setTimeout(() => controller.abort(), 10)
await expect(promise).rejects.toThrow('Operation aborted')
})
it('should cleanup event listener when finallyPromise completes', async () => {
const controller = new AbortController()
const finallyPromise = Promise.resolve('completed')
const removeEventListenerSpy = vi.spyOn(controller.signal, 'removeEventListener')
createAbortPromise(controller.signal, finallyPromise)
// 等待 finallyPromise 完成
await finallyPromise
// 给一点时间让 finally 回调执行
await new Promise((resolve) => setTimeout(resolve, 0))
// 验证清理工作
expect(removeEventListenerSpy).toHaveBeenCalledWith('abort', expect.any(Function))
})
it('should not reject when finallyPromise resolves normally', async () => {
// 测试正常完成情况
const controller = new AbortController()
const finallyPromise = Promise.resolve('success')
// createAbortPromise 返回的是一个永远 pending 的 Promise除非被 abort
const abortPromise = createAbortPromise(controller.signal, finallyPromise)
// 让 finallyPromise 完成
await finallyPromise
// abortPromise 应该保持 pending 状态(因为没有被 abort
// 我们不能直接测试 pending 状态,但可以确保它不会立即 reject
let rejected = false
abortPromise.catch(() => {
rejected = true
})
await new Promise((resolve) => setTimeout(resolve, 10))
expect(rejected).toBe(false)
})
it('should handle signal that becomes aborted before Promise creation', () => {
// 测试在创建 Promise 前就已经 aborted 的信号
const controller = new AbortController()
controller.abort()
const finallyPromise = new Promise<string>(() => {}) // 永不解析
const promise = createAbortPromise(controller.signal, finallyPromise)
return expect(promise).rejects.toMatchObject({
name: 'AbortError',
message: 'Operation aborted'
})
})
})
})

View File

@ -8,9 +8,14 @@ export const addAbortController = (id: string, abortFn: () => void) => {
export const removeAbortController = (id: string, abortFn: () => void) => {
const callbackArr = abortMap.get(id)
if (abortFn) {
callbackArr?.splice(callbackArr?.indexOf(abortFn), 1)
} else abortMap.delete(id)
if (abortFn && callbackArr) {
const index = callbackArr.indexOf(abortFn)
if (index !== -1) {
callbackArr.splice(index, 1)
}
} else {
abortMap.delete(id)
}
}
export const abortCompletion = (id: string) => {