diff --git a/db.ts b/db.ts index 9e05e51..23357d3 100644 --- a/db.ts +++ b/db.ts @@ -524,6 +524,11 @@ export class EhDb { ); return s.length ? s[0] : undefined; } + get_tasks() { + return this.transaction(() => + this.db.queryEntries("SELECT * FROM task;") + ); + } get_tasks_by_pid(pid: number) { return this.transaction(() => this.db.queryEntries("SELECT * FROM task WHERE pid = ?;", [ diff --git a/deno.lock b/deno.lock index dc00555..b48736e 100644 --- a/deno.lock +++ b/deno.lock @@ -197,6 +197,7 @@ "https://esm.sh/*@preact/signals@1.1.3": "f1591d7185a00b6f96fdf5f72a99bb7dde37c0e946c8854da71db6b99d430947", "https://esm.sh/*preact-render-to-string@5.2.6": "88ec8d8706b6a3f1e0fdad3862a2690dcd9b350d87bdc8e7bd0e27fbc0f7d29e", "https://esm.sh/accept-language-parser@1.5.0/": "16d82ee0a75451f75b42d9a20db4da0ccae7ccc8cc09a41c73b4488aba010b94", + "https://esm.sh/gh/SortableJS/Sortable@1.15.0/Sortable.min.js": "0a3e3cf471bf4566d7a3f9823b1d0d3b2bd075404e93419cd4074bd23310a9b0", "https://esm.sh/preact-material-components@1.6.1/Button": "bc60923d511c6e2e33a7064339b3e643a9c15e3ef232ab063ef570af2ef83dc8", "https://esm.sh/preact-material-components@1.6.1/Checkbox": "bf34f5cd8c6d015916d854d91aab2caf115463e97be9a461f8dd3370ea11a49c", "https://esm.sh/preact-material-components@1.6.1/Dialog": "b0ff8da9c770456748f7e065fecda2fc90f5364ea66cae75ff5f51d57f6a87eb", @@ -319,6 +320,7 @@ "https://esm.sh/v124/twind@0.16.19/deno/twind.mjs": "ac4bf729653ee66349a518ee4949f22e84b7f626e8f74de1066e798a7c1ca12a", "https://esm.sh/v124/twind@0.16.19/sheets/sheets.d.ts": "9cd4663d180023e49d9379777c8926347f3f79bf3bee546e7e5be1524fe55e70", "https://esm.sh/v124/twind@0.16.19/twind.d.ts": "48c49da7d770f1236ec8a9397af053e6fb5a2bedacf431f2189eeecc1468c01b", + "https://esm.sh/v125/gh/SortableJS/Sortable@1.15.0/denonext/Sortable.min.js": "aeba191bb6622c4ad41ae5d8f5d4dd6bf15074f264cb3640ed223fbaf052263b", "https://raw.githubusercontent.com/lucacasonato/esbuild_deno_loader/8031f71afa1bbcd3237a94b11f53a2e5c5c0e7bf/deps.ts": "b7248e5b750be62613a9417f407e65ed43726d83b11f9631d6dbb58634bbd7d1", "https://raw.githubusercontent.com/lucacasonato/esbuild_deno_loader/8031f71afa1bbcd3237a94b11f53a2e5c5c0e7bf/mod.ts": "3e507379372361162f93325a216b86f6098defb5bb60144555b507bca26d061f", "https://raw.githubusercontent.com/lucacasonato/esbuild_deno_loader/8031f71afa1bbcd3237a94b11f53a2e5c5c0e7bf/src/deno.ts": "71bee6b14e72ca193c0686d8b4f1f47d639a64745b6f5c7576f7a3616f436f57", diff --git a/fresh.gen.ts b/fresh.gen.ts index b8a0d9f..ea28c4a 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -10,6 +10,7 @@ import * as $3 from "./routes/api/task.ts"; import * as $4 from "./routes/index.tsx"; import * as $$0 from "./islands/Container.tsx"; import * as $$1 from "./islands/Settings.tsx"; +import * as $$2 from "./islands/TaskManager.tsx"; const manifest = { routes: { @@ -22,6 +23,7 @@ const manifest = { islands: { "./islands/Container.tsx": $$0, "./islands/Settings.tsx": $$1, + "./islands/TaskManager.tsx": $$2, }, baseUrl: import.meta.url, config, diff --git a/import_map.json b/import_map.json index f7bef00..f0ba8d1 100644 --- a/import_map.json +++ b/import_map.json @@ -14,6 +14,7 @@ "twind/": "https://esm.sh/twind@0.16.19/", "$std/": "https://deno.land/std@0.187.0/", "preact-material-components/": "https://esm.sh/preact-material-components@1.6.1/", - "accept-language-parser/": "https://esm.sh/accept-language-parser@1.5.0/" + "accept-language-parser/": "https://esm.sh/accept-language-parser@1.5.0/", + "sortable": "https://esm.sh/gh/SortableJS/Sortable@1.15.0/Sortable.min.js" } } diff --git a/islands/Container.tsx b/islands/Container.tsx index f3c2e69..d267445 100644 --- a/islands/Container.tsx +++ b/islands/Container.tsx @@ -8,6 +8,7 @@ import StyleSheet from "../components/StyleSheet.tsx"; import { GlobalCtx } from "../components/GlobalContext.tsx"; import Settings from "./Settings.tsx"; import t, { i18n_map, I18NMap } from "../server/i18n.ts"; +import TaskManager from "./TaskManager.tsx"; export type ContainerProps = { i18n: I18NMap; @@ -76,6 +77,14 @@ export default class Container extends Component { > home + { + set_display(false); + set_state("#/task_manager"); + }} + > + task + { set_display(false); @@ -87,6 +96,7 @@ export default class Container extends Component {
+
); diff --git a/islands/TaskManager.tsx b/islands/TaskManager.tsx new file mode 100644 index 0000000..10c66a1 --- /dev/null +++ b/islands/TaskManager.tsx @@ -0,0 +1,79 @@ +import { Component, ContextType } from "preact"; +import { useEffect, useRef, useState } from "preact/hooks"; +import { GlobalCtx } from "../components/GlobalContext.tsx"; +import { TaskDetail, TaskStatus } from "../task.ts"; +import { Sortable } from "sortable"; +import { TaskClientSocketData, TaskServerSocketData } from "../server/task.ts"; +import { get_ws_host } from "../server/utils.ts"; + +export type TaskManagerProps = { + show: boolean; +}; + +export default class TaskManager extends Component { + static contextType = GlobalCtx; + declare context: ContextType; + render() { + if (!this.props.show) return null; + const [tasks, set_tasks] = useState>(new Map()); + const ul = useRef(); + useEffect(() => { + new Sortable(ul.current, { + onSort: (evt: CustomEvent) => { + console.log(evt); + }, + }); + const ws = new WebSocket(`${get_ws_host()}/api/task`); + console.log(ws); + function sendMessage(mes: TaskClientSocketData) { + ws.send(JSON.stringify(mes)); + } + ws.onopen = () => { + sendMessage({ type: "task_list" }); + }; + ws.onmessage = (e) => { + const t: TaskServerSocketData = JSON.parse(e.data); + if (t.type == "close") { + ws.close(); + } else if (t.type == "tasks") { + set_tasks((tasks) => { + t.tasks.forEach((ta) => { + tasks.set(ta.id, { + base: ta, + status: t.running.includes(ta.id) + ? TaskStatus.Running + : TaskStatus.Wait, + }); + }); + this.forceUpdate(); + return tasks; + }); + } else if (t.type == "new_task") { + set_tasks((tasks) => { + tasks.set(t.detail.id, { + base: t.detail, + status: TaskStatus.Wait, + }); + this.forceUpdate(); + return tasks; + }); + } + }; + self.addEventListener("beforeunload", () => { + sendMessage({ type: "close" }); + }); + }, []); + console.log(tasks.size); + return ( +
+
+ {Array.from(tasks.keys()).map((k) =>
{k}
)} +
+
+ ); + } +} diff --git a/routes/api/task.ts b/routes/api/task.ts index efdffa2..0d14a13 100644 --- a/routes/api/task.ts +++ b/routes/api/task.ts @@ -1,15 +1,10 @@ import { Handlers } from "$fresh/server.ts"; import { get_task_manager } from "../../server.ts"; import { Task, TaskProgress } from "../../task.ts"; -import { DiscriminatedUnion } from "../../utils.ts"; - -type EventMap = { - close: Record; - new_download_task: { gid: number; token: string }; - new_export_zip_task: { gid: number; output?: string }; -}; - -type EventData = DiscriminatedUnion<"type", EventMap>; +import { + TaskClientSocketData, + TaskServerSocketData, +} from "../../server/task.ts"; export const handler: Handlers = { GET(req, _ctx) { @@ -24,6 +19,9 @@ export const handler: Handlers = { t.removeEventListener("task_finished", handle); t.removeEventListener("task_progress", handle); }; + function sendMessage(mes: TaskServerSocketData) { + socket.send(JSON.stringify(mes)); + } socket.onclose = () => { removeListener(); }; @@ -33,13 +31,22 @@ export const handler: Handlers = { }; socket.onmessage = (e) => { try { - const d: EventData = JSON.parse(e.data); + const d: TaskClientSocketData = JSON.parse(e.data); if (d.type == "close") { + sendMessage({ type: "close" }); socket.close(); } else if (d.type == "new_download_task") { t.add_download_task(d.gid, d.token); } else if (d.type == "new_export_zip_task") { t.add_export_zip_task(d.gid, d.output); + } else if (d.type == "task_list") { + t.get_task_list().then((tasks) => { + sendMessage({ + type: "tasks", + tasks, + running: t.get_running_task(), + }); + }); } } catch (_) { null; diff --git a/server/task.ts b/server/task.ts new file mode 100644 index 0000000..23e8217 --- /dev/null +++ b/server/task.ts @@ -0,0 +1,18 @@ +import { Task } from "../task.ts"; +import { TaskEventData } from "../task_manager.ts"; +import { DiscriminatedUnion } from "../utils.ts"; + +export type TaskServerSocketData = TaskEventData | { type: "close" } | { + type: "tasks"; + tasks: Task[]; + running: number[]; +}; + +type EventMap = { + new_download_task: { gid: number; token: string }; + new_export_zip_task: { gid: number; output?: string }; +}; + +export type TaskClientSocketData = DiscriminatedUnion<"type", EventMap> | { + type: "close"; +} | { type: "task_list" }; diff --git a/server/utils.ts b/server/utils.ts new file mode 100644 index 0000000..4346361 --- /dev/null +++ b/server/utils.ts @@ -0,0 +1,4 @@ +export function get_ws_host() { + const protocol = document.location.protocol === "https:" ? "wss:" : "ws:"; + return `${protocol}//${document.location.host}`; +} diff --git a/task.ts b/task.ts index c6d10ad..b9d7ecc 100644 --- a/task.ts +++ b/task.ts @@ -5,9 +5,9 @@ export enum TaskType { ExportZip, } -export type Task = { +export type Task = { id: number; - type: TaskType; + type: T; gid: number; token: string; pid: number; @@ -30,9 +30,23 @@ type TaskId> = { } & T[P]) extends infer U ? { [Q in keyof U]: U[Q] } : never; }; -export type TaskProgressType = TaskId<{ +export type TaskProgressBasicType = { [TaskType.Download]: TaskDownloadProgess; [TaskType.ExportZip]: TaskExportZipProgress; -}>; +}; + +export type TaskProgressType = TaskId; export type TaskProgress = DiscriminatedUnion<"type", TaskProgressType>; + +export enum TaskStatus { + Wait, + Running, + Finished, +} + +export type TaskDetail = { + base: Task; + progress?: TaskProgressBasicType[T]; + status: TaskStatus; +}; diff --git a/task_manager.ts b/task_manager.ts index b6ef74f..4bbcff7 100644 --- a/task_manager.ts +++ b/task_manager.ts @@ -33,12 +33,17 @@ type Detail> = { export type TaskEventData = DiscriminatedUnion<"type", Detail>; +type RunningTask = { + task: Promise; + base: Task; +}; + export class TaskManager extends EventTarget { #closed = false; cfg; client; db; - running_tasks: Map>; + running_tasks: Map; max_task_count; #abort; #force_abort; @@ -131,7 +136,7 @@ export class TaskManager extends EventTarget { this.#check_closed(); const removed_task: number[] = []; for (const [id, task] of this.running_tasks) { - const status = await promiseState(task); + const status = await promiseState(task.task); if (status.status == PromiseStatus.Fulfilled && status.value) { removed_task.push(id); await this.db.delete_task(status.value); @@ -175,6 +180,12 @@ export class TaskManager extends EventTarget { get force_aborts() { return this.#force_abort.signal; } + get_running_task() { + return Array.from(this.running_tasks.keys()); + } + get_task_list() { + return this.db.get_tasks(); + } // @ts-ignore Checked type removeEventListener( type: T, @@ -221,15 +232,18 @@ export class TaskManager extends EventTarget { if (task.type == TaskType.Download) { this.running_tasks.set( task.id, - download_task( - task, - this.client, - this.db, - this.cfg, - this.#abort.signal, - this.#force_abort.signal, - this, - ), + { + task: download_task( + task, + this.client, + this.db, + this.cfg, + this.#abort.signal, + this.#force_abort.signal, + this, + ), + base: task, + }, ); } else if (task.type == TaskType.ExportZip) { const cfg: ExportZipConfig = task.details @@ -237,14 +251,17 @@ export class TaskManager extends EventTarget { : DEFAULT_EXPORT_ZIP_CONFIG; this.running_tasks.set( task.id, - export_zip( - task, - this.db, - this.cfg, - this.#abort.signal, - cfg, - this, - ), + { + task: export_zip( + task, + this.db, + this.cfg, + this.#abort.signal, + cfg, + this, + ), + base: task, + }, ); } }