mirror of
https://github.com/rustfs/console.git
synced 2026-08-31 01:37:52 +08:00
274 lines
8.4 KiB
TypeScript
274 lines
8.4 KiB
TypeScript
export type TaskEventType = "enqueued" | "running" | "completed" | "failed" | "canceled" | "drained"
|
|
|
|
export type TaskEventHandler<TTask> = (task: TTask) => void
|
|
export type AllCompletedEventHandler = () => void
|
|
|
|
export interface TaskLifecycleStatus<TStatus extends string> {
|
|
pending: TStatus
|
|
running: TStatus
|
|
completed: TStatus
|
|
failed: TStatus
|
|
canceled: TStatus
|
|
paused?: TStatus
|
|
}
|
|
|
|
export interface ManagedTask<TStatus extends string> {
|
|
id: string
|
|
status: TStatus
|
|
progress: number
|
|
error?: string
|
|
abortController?: AbortController
|
|
retryCount?: number
|
|
kind: string
|
|
}
|
|
|
|
export interface TaskHandler<TTask, TStatus extends string> {
|
|
lifecycle: TaskLifecycleStatus<TStatus>
|
|
perform: (task: TTask) => Promise<void>
|
|
shouldRetry?: (task: TTask, error: unknown) => boolean
|
|
handleError?: (task: TTask, error: unknown) => Promise<"retry" | "handled" | "fail"> | "retry" | "handled" | "fail"
|
|
isCanceledError?: (error: unknown) => boolean
|
|
maxRetries?: number
|
|
retryDelay?: number
|
|
}
|
|
|
|
export interface TaskManagerOptions<TTask extends ManagedTask<TStatus>, TStatus extends string> {
|
|
handlers: Record<string, TaskHandler<TTask, TStatus>>
|
|
maxConcurrent?: number
|
|
maxRetries?: number
|
|
retryDelay?: number
|
|
}
|
|
|
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
|
|
type Listener = () => void
|
|
|
|
export class TaskManager<TTask extends ManagedTask<TStatus>, TStatus extends string> {
|
|
private tasks: TTask[] = []
|
|
private readonly handlers: Record<string, TaskHandler<TTask, TStatus>>
|
|
readonly maxConcurrent: number
|
|
readonly maxRetries: number
|
|
readonly retryDelay: number
|
|
private activeCount = 0
|
|
private isStarted = false
|
|
private allCompletedEmitted = false
|
|
private listeners = new Set<Listener>()
|
|
private snapshotCache: TTask[] | null = null
|
|
|
|
constructor(options: TaskManagerOptions<TTask, TStatus>) {
|
|
this.handlers = options.handlers
|
|
this.maxConcurrent = options.maxConcurrent ?? 6
|
|
this.maxRetries = options.maxRetries ?? 3
|
|
this.retryDelay = options.retryDelay ?? 1000
|
|
}
|
|
|
|
subscribe(listener: Listener): () => void {
|
|
this.listeners.add(listener)
|
|
return () => this.listeners.delete(listener)
|
|
}
|
|
|
|
private notify() {
|
|
this.snapshotCache = null
|
|
this.listeners.forEach((l) => l())
|
|
}
|
|
|
|
enqueue(tasks: TTask[]) {
|
|
if (!tasks.length) return
|
|
this.allCompletedEmitted = false
|
|
this.tasks.push(...tasks)
|
|
this.notify()
|
|
this.start()
|
|
}
|
|
|
|
start() {
|
|
if (!this.isStarted) this.isStarted = true
|
|
this.processQueue()
|
|
}
|
|
|
|
cancel() {
|
|
this.tasks.forEach((task) => {
|
|
const lifecycle = this.getLifecycle(task) as TaskLifecycleStatus<TStatus>
|
|
if (this.isActive(task)) {
|
|
;(task as TTask & { _pauseRequested?: boolean })._pauseRequested = false
|
|
task.abortController?.abort()
|
|
task.status = lifecycle.canceled
|
|
}
|
|
})
|
|
this.notify()
|
|
this.checkAllCompleted()
|
|
}
|
|
|
|
cancelTask(taskId: string) {
|
|
const task = this.tasks.find((t) => t.id === taskId)
|
|
if (!task) return
|
|
const lifecycle = this.getLifecycle(task) as TaskLifecycleStatus<TStatus>
|
|
;(task as TTask & { _pauseRequested?: boolean })._pauseRequested = false
|
|
task.abortController?.abort()
|
|
task.status = lifecycle.canceled
|
|
this.notify()
|
|
this.checkAllCompleted()
|
|
}
|
|
|
|
removeTask(taskId: string) {
|
|
const index = this.tasks.findIndex((t) => t.id === taskId)
|
|
if (index === -1) return
|
|
const task = this.tasks[index]
|
|
if (task && this.isActive(task)) {
|
|
task.abortController?.abort()
|
|
}
|
|
this.tasks.splice(index, 1)
|
|
this.notify()
|
|
}
|
|
|
|
clearTasks() {
|
|
this.cancel()
|
|
this.tasks = []
|
|
this.activeCount = 0
|
|
this.isStarted = false
|
|
this.notify()
|
|
}
|
|
|
|
getTasks(): TTask[] {
|
|
if (this.snapshotCache) return this.snapshotCache
|
|
this.snapshotCache = [...this.tasks]
|
|
return this.snapshotCache
|
|
}
|
|
|
|
private isActive(task: TTask) {
|
|
const lifecycle = this.getLifecycle(task) as TaskLifecycleStatus<TStatus>
|
|
const activeStatuses = [lifecycle.pending, lifecycle.running]
|
|
if (lifecycle.paused) activeStatuses.push(lifecycle.paused)
|
|
return activeStatuses.includes(task.status)
|
|
}
|
|
|
|
private checkAllCompleted() {
|
|
const hasActive = this.tasks.some((task) => this.isActive(task))
|
|
if (!hasActive && this.tasks.length > 0 && !this.allCompletedEmitted) {
|
|
this.allCompletedEmitted = true
|
|
this.notify()
|
|
}
|
|
}
|
|
|
|
private processQueue() {
|
|
if (!this.isStarted) return
|
|
|
|
while (this.activeCount < this.maxConcurrent) {
|
|
const next = this.tasks.find((task) => {
|
|
const handler = this.handlers[task.kind]
|
|
return handler && task.status === (handler.lifecycle as TaskLifecycleStatus<TStatus>).pending
|
|
})
|
|
if (!next) break
|
|
this.activeCount++
|
|
this.runTask(next)
|
|
}
|
|
|
|
const hasPending = this.tasks.some((task) => {
|
|
const lifecycle = this.getLifecycle(task) as TaskLifecycleStatus<TStatus>
|
|
return task.status === lifecycle.pending
|
|
})
|
|
if (hasPending) {
|
|
setTimeout(() => this.processQueue(), 100)
|
|
} else {
|
|
setTimeout(() => this.checkAllCompleted(), 200)
|
|
}
|
|
}
|
|
|
|
private watchProgress(task: TTask, lifecycle: TaskLifecycleStatus<TStatus>) {
|
|
let lastProgress = task.progress
|
|
const timer = setInterval(() => {
|
|
if (task.status !== lifecycle.running) return
|
|
if (task.progress === lastProgress) return
|
|
lastProgress = task.progress
|
|
this.notify()
|
|
}, 120)
|
|
return () => clearInterval(timer)
|
|
}
|
|
|
|
private async runTask(task: TTask) {
|
|
const lifecycle = this.getLifecycle(task) as TaskLifecycleStatus<TStatus>
|
|
task.status = lifecycle.running
|
|
this.notify()
|
|
const stopWatchingProgress = this.watchProgress(task, lifecycle)
|
|
|
|
try {
|
|
const handler = this.getHandler(task)
|
|
await handler.perform(task)
|
|
if (task.status === lifecycle.running) {
|
|
task.status = lifecycle.completed
|
|
task.progress = 100
|
|
}
|
|
this.notify()
|
|
this.checkAllCompleted()
|
|
} catch (error) {
|
|
const action = await this.handleError(task, error)
|
|
if (action === "retry") {
|
|
await this.retryTask(task)
|
|
} else {
|
|
task.status = lifecycle.failed
|
|
task.error = (error as Error)?.message ?? "Unknown error"
|
|
}
|
|
this.notify()
|
|
this.checkAllCompleted()
|
|
} finally {
|
|
stopWatchingProgress()
|
|
this.activeCount--
|
|
if (this.isStarted) setTimeout(() => this.processQueue(), 50)
|
|
}
|
|
}
|
|
|
|
private async handleError(task: TTask, error: unknown): Promise<"retry" | "handled" | "fail"> {
|
|
const handler = this.getHandler(task)
|
|
if (handler.handleError) {
|
|
const result = await handler.handleError(task, error)
|
|
if (result !== "fail") return result
|
|
}
|
|
if (this.isCanceledError(task, error)) {
|
|
const lifecycle = this.getLifecycle(task) as TaskLifecycleStatus<TStatus>
|
|
task.status = lifecycle.canceled
|
|
this.notify()
|
|
this.checkAllCompleted()
|
|
return "handled"
|
|
}
|
|
if (this.shouldRetry(task, error)) return "retry"
|
|
return "fail"
|
|
}
|
|
|
|
private shouldRetry(task: TTask, error: unknown) {
|
|
const handler = this.getHandler(task)
|
|
const lifecycle = this.getLifecycle(task) as TaskLifecycleStatus<TStatus>
|
|
const maxRetries = handler.maxRetries ?? this.maxRetries
|
|
if (task.status === lifecycle.canceled) return false
|
|
if ((task.retryCount || 0) >= maxRetries) return false
|
|
if (handler.shouldRetry) return handler.shouldRetry(task, error)
|
|
return true
|
|
}
|
|
|
|
private isCanceledError(task: TTask, error: unknown) {
|
|
const handler = this.getHandler(task)
|
|
if (handler.isCanceledError) return handler.isCanceledError(error)
|
|
return (error as Error)?.name === "AbortError" || (error as Error)?.message?.includes("canceled")
|
|
}
|
|
|
|
private async retryTask(task: TTask) {
|
|
const handler = this.getHandler(task)
|
|
const lifecycle = this.getLifecycle(task) as TaskLifecycleStatus<TStatus>
|
|
const retryDelay = handler.retryDelay ?? this.retryDelay
|
|
task.retryCount = (task.retryCount || 0) + 1
|
|
task.status = lifecycle.pending
|
|
task.progress = 0
|
|
task.error = undefined
|
|
const retryCount = task.retryCount || 1
|
|
await sleep(retryDelay * retryCount)
|
|
}
|
|
|
|
private getHandler(task: TTask): TaskHandler<TTask, TStatus> {
|
|
const handler = this.handlers[task.kind] as TaskHandler<TTask, TStatus> | undefined
|
|
if (!handler) throw new Error(`No task handler registered for kind: ${task.kind}`)
|
|
return handler
|
|
}
|
|
|
|
private getLifecycle(task: TTask) {
|
|
return this.getHandler(task).lifecycle
|
|
}
|
|
}
|