clapper / src /components /tasks /useTasks.tsx
jbilcke-hf's picture
jbilcke-hf HF staff
add linter and prettifier
c1f12bf
raw
history blame
No virus
10.9 kB
'use client'
import { create } from 'zustand'
import { toast } from 'sonner'
import { UUID } from '@aitube/clap'
import {
TasksStore,
NewTask,
Task,
TaskCategory,
TaskProgressType,
TaskRemoteControl,
TaskStatus,
TaskVisibility,
} from '@aitube/clapper-services'
import { TaskStatusUpdate } from './TaskStatusUpdate'
import { sleep } from '@/lib/utils/sleep'
// we enforce the impossibility of some transitions
function statusTransition(
current: TaskStatus,
requested: TaskStatus
): TaskStatus {
switch (current) {
case TaskStatus.UPCOMING:
case TaskStatus.RUNNING:
case TaskStatus.PAUSED:
return requested
default:
return current
}
}
/**
* A task manager, used for downloads, processing..
*
* It may seem a bit complicated, but that's because it can handle various modes:
*
* - Promise (you pass a function returning a promise, and it will wait the end)
* - Progressive (no function, no promise, instead you call yourself a progress update function)
*
* It is also complex, because it is bidirectional!
* - A task can receive a helper to gets its status, but it can also sets its own status
* - A task can finish itself, ir be finished by an external event etc..
*
* and it also provides tons of helper to track the status
*/
export const useTasks = create<TasksStore>((set, get) => ({
tasks: {} as Record<string, Task>,
expandTasks: false,
setExpandTasks: (expandTasks: boolean) => {
set({ expandTasks })
},
get: (taskId?: string): TaskRemoteControl | undefined => {
const { tasks } = get()
if (!taskId) {
return undefined
}
const task = (tasks[taskId || ''] || undefined) as Task | undefined
if (!task) {
return
}
// we provide a helper to whoever called us
return {
task,
promise: task.promise,
pause: () => {
return get().pause(task.id)
},
continue: () => {
return get().continue(task.id)
},
setStatus: (status: TaskStatus) => {
return get().setStatus(status, task.id)
},
setProgress: async (options?: {
value?: number
sleepDelay?: number
message?: string
isFinished?: boolean
hasFailed?: boolean
}) => {
return get().setProgress(task.id, options)
},
success: () => {
return get().success(task.id)
},
fail: (reason?: string) => {
return get().fail(task.id, reason)
},
cancel: () => {
return get().cancel(task.id)
},
}
},
find: (params?: {
status?: TaskStatus
category?: TaskCategory
visibility?: TaskVisibility
}): Task[] => {
const { tasks } = get()
let list = Object.values(tasks)
if (params?.status) {
list = list.filter((t) => t.status === params?.status)
}
if (params?.category) {
list = list.filter((t) => t.category === params?.category)
}
return list
},
add: (
partialTask: Partial<NewTask>,
status?: TaskStatus
): TaskRemoteControl => {
const mode: TaskProgressType =
partialTask?.mode || TaskProgressType.PERCENTAGE
const min =
mode === TaskProgressType.COUNTER && partialTask?.min
? partialTask.min
: 0
const max =
mode === TaskProgressType.COUNTER && partialTask?.max
? partialTask.max
: mode === TaskProgressType.RATIO
? 1
: mode === TaskProgressType.PERCENTAGE
? 100
: 100
const id = UUID()
const newTask: NewTask = {
id,
visibility: partialTask?.visibility || TaskVisibility.BACKGROUND,
category: partialTask?.category || TaskCategory.GENERIC,
initialMessage: partialTask?.initialMessage || 'Loading..',
successMessage: partialTask?.successMessage || 'Task completed!',
priority: partialTask?.priority || 0, // 0 = lowest, 1 or more = more and more important
status: status || TaskStatus.RUNNING,
value: partialTask?.value || 0,
progress: partialTask?.progress || 0,
min,
max,
mode,
run: partialTask.run,
}
const task: Task = {
...newTask,
currentMessage: newTask.initialMessage,
startedAt: new Date().toISOString(),
endedAt: '',
promise: Promise.resolve(TaskStatus.RUNNING),
}
task.promise = new Promise<TaskStatus>((resolve, reject) => {
let checkStatus = () => {
try {
// need to use fresh data here
const t = get().get(id)!
if (!t) {
resolve(TaskStatus.SUCCESS)
return
}
const status = t.task.status || 'deleted'
const progress = t.task.progress || 0
console.log(
`useTasks[${id}]: checkStatus: checking task, current status is: "${status}"`
)
if (
status === TaskStatus.ERROR ||
status === TaskStatus.SUCCESS ||
status === TaskStatus.DELETED ||
status === TaskStatus.CANCELLED
) {
console.log(
`useTasks[${id}]: checkStatus: status is "${status}", interrupting task loop..`
)
// this call might be redundant
if (status === TaskStatus.SUCCESS) {
get().setProgress(id, { isFinished: true })
}
resolve(status)
} else if (progress >= 100) {
console.log(
`useTasks[${id}]: checkStatus: task is completed at 100%, interrupting task loop..`
)
// this call might be redundant
get().setProgress(id, { isFinished: true })
// get().setStatus(TaskStatus.SUCCESS, id)
resolve(TaskStatus.SUCCESS)
} else {
console.log(
`useTasks[${id}]: checkStatus: status is "${status}", continuing task loop..`
)
setTimeout(checkStatus, 1000)
}
} catch (err) {
console.error('useTasks:checkStatus: ', err)
}
}
checkStatus()
})
toast.promise<TaskStatus>(task.promise, {
loading: <TaskStatusUpdate taskId={id} />,
success: (finalStatus) => {
return finalStatus === TaskStatus.SUCCESS
? task.successMessage
: `Task ended`
},
error: 'Task aborted',
})
const { tasks } = get()
set({
tasks: {
...tasks,
[id]: task,
},
})
setTimeout(async () => {
// optionally launch the task function, if there is one
if (!task.run) {
return
}
// oh, one last thing: let's launch-and-forget the actual task
console.log(
`useTasks[${id}]: launching the task runner in the background..`
)
// we provide to the task runner a wait to get the current status
// that wait long-running jobs will know when they have been cancelled and no longer needed
const result = await task.run(() => {
const remoteControl = get().get(id)!
const status = remoteControl?.task?.status
console.log(
`useTasks[${id}]: task runner asked for current status (which is: "${status || 'deleted'}")`
)
return status || 'deleted'
})
console.log(`useTasks[${id}]: task runner ended with status: "${result}"`)
get().setProgress(id, { isFinished: true })
// get().setStatus(result, id)
}, 100)
// we want to return the "remote control", which is a more complex object,
// with functions and all
// the easiest way to do this is to call our get() function
const remoteControl = get().get(id)!
return remoteControl
},
pause: (taskId?: string) => {
get().setStatus(TaskStatus.PAUSED, taskId)
},
continue: (taskId?: string) => {
get().setStatus(TaskStatus.RUNNING, taskId)
},
setStatus: (status: TaskStatus, taskId?: string) => {
const { tasks } = get()
const task = get().get(taskId)?.task
console.log(`useTasks[${taskId}]:setStatus("${status}")`)
if (task) {
console.log(
`useTasks[${taskId}]:setStatus("${status}") -> setting one task to ${status}`
)
set({
tasks: {
...tasks,
[task.id]: { ...task, status: statusTransition(task.status, status) },
},
})
} else {
console.log(
`useTasks[${taskId}]:setStatus("${status}") -> setting all tasks to ${status}`
)
const newTasks = {} as Record<string, Task>
for (const [id, t] of Object.entries(tasks)) {
newTasks[id] = { ...t, status: statusTransition(t.status, status) }
}
set({
tasks: newTasks,
})
}
},
setProgress: async (
taskId: string,
options?: {
value?: number
sleepDelay?: number
message?: string
isFinished?: boolean
hasFailed?: boolean
}
): Promise<void> => {
try {
const { tasks } = get()
const task = get().get(taskId)?.task
const message = options?.message || ''
const value = options?.value || task?.value || 0
const sleepDelay = options?.sleepDelay || 100
if (task) {
let progress =
task.mode === TaskProgressType.PERCENTAGE
? value
: task.mode === TaskProgressType.RATIO
? value * 100
: ((value - task.min) / (task.max - task.min)) * 100
const currentMessage = message || task.initialMessage
if (options?.hasFailed) {
set({
tasks: {
...tasks,
[task.id]: {
...task,
currentMessage,
status: TaskStatus.ERROR,
endedAt: new Date().toISOString(),
},
},
})
} else {
const isFinished = options?.isFinished || progress >= 100
if (isFinished) {
progress = 100
}
set({
tasks: {
...tasks,
[task.id]: {
...task,
progress,
value,
currentMessage,
status: isFinished ? TaskStatus.SUCCESS : TaskStatus.RUNNING,
endedAt: isFinished ? new Date().toISOString() : task.endedAt,
},
},
})
}
await sleep(sleepDelay)
}
} catch (err) {}
},
clear: () => {
get().cancel()
set({ tasks: {} })
},
success: (taskId: string) => {
get().setProgress(taskId, { isFinished: true })
get().setStatus(TaskStatus.SUCCESS, taskId)
},
fail: (taskId: string, reason?: string) => {
get().setProgress(taskId, {
message: reason || 'unknown failure',
isFinished: true,
hasFailed: true,
})
get().setStatus(TaskStatus.ERROR, taskId)
},
cancel: (taskId?: string) => {
get().setStatus(TaskStatus.CANCELLED, taskId)
},
}))