返回 JoyAI-Echo
imageEncode.ts
根目录 / echo_longvideo / Director_Agent / webui / src / lib / imageEncode.ts
1 /**
2 * Main-thread client for the image encoder Worker.
3 *
4 * Lazily boots a single ``imageEncode.worker`` and multiplexes requests onto
5 * it by a random request id. Falls back to an inline call when the Worker
6 * can't be constructed (tests, ancient browsers) so the Composer always has a
7 * working path.
8 */
9 import {
10 encodeImageInWorker,
11 type EncodeResponse,
12 } from "@/workers/imageEncode.worker";
13
14 export type { EncodeResponse, EncodeSuccess, EncodeFailure } from "@/workers/imageEncode.worker";
15 export { TARGET_MAX_BYTES } from "@/workers/imageEncode.worker";
16
17 type Pending = {
18 resolve: (r: EncodeResponse) => void;
19 reject: (err: Error) => void;
20 };
21
22 let worker: Worker | null = null;
23 let bootAttempted = false;
24 const pending = new Map<string, Pending>();
25
26 function bootWorker(): Worker | null {
27 if (bootAttempted) return worker;
28 bootAttempted = true;
29 if (typeof Worker === "undefined") return null;
30 try {
31 worker = new Worker(
32 new URL("@/workers/imageEncode.worker.ts", import.meta.url),
33 { type: "module" },
34 );
35 worker.addEventListener("message", (ev: MessageEvent<EncodeResponse>) => {
36 const entry = pending.get(ev.data.id);
37 if (!entry) return;
38 pending.delete(ev.data.id);
39 entry.resolve(ev.data);
40 });
41 worker.addEventListener("error", (ev) => {
42 // Cancel every in-flight request on a Worker crash.
43 for (const [, entry] of pending) {
44 entry.reject(new Error(`image encoder worker error: ${ev.message}`));
45 }
46 pending.clear();
47 worker?.terminate();
48 worker = null;
49 });
50 return worker;
51 } catch {
52 worker = null;
53 return null;
54 }
55 }
56
57 function newId(): string {
58 // ``crypto.randomUUID`` is widely available; fall back to Math.random if not.
59 if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
60 return (crypto as Crypto).randomUUID();
61 }
62 return `img-${Date.now()}-${Math.random().toString(36).slice(2)}`;
63 }
64
65 /** Encode ``file`` off the main thread when possible. Always resolves — errors
66 * are returned as ``{ok: false, reason}`` — so callers can render inline
67 * validation without wrapping in try/catch. */
68 export async function encodeImage(file: File): Promise<EncodeResponse> {
69 const id = newId();
70 const w = bootWorker();
71 if (!w) {
72 // Inline fallback: same logic, just on the main thread.
73 return encodeImageInWorker({ id, file });
74 }
75 return new Promise<EncodeResponse>((resolve, reject) => {
76 pending.set(id, { resolve, reject });
77 try {
78 w.postMessage({ id, file });
79 } catch (err) {
80 pending.delete(id);
81 reject(err instanceof Error ? err : new Error(String(err)));
82 }
83 });
84 }
85
86 /** Release the singleton Worker (tests / teardown). */
87 export function disposeImageEncoder(): void {
88 if (worker) {
89 worker.terminate();
90 worker = null;
91 }
92 bootAttempted = false;
93 for (const [, entry] of pending) {
94 entry.reject(new Error("image encoder disposed"));
95 }
96 pending.clear();
97 }
98
98 lines TYPESCRIPT