feat(webui): support image uploads in composer and message bubbles
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Main-thread client for the image encoder Worker.
|
||||
*
|
||||
* Lazily boots a single ``imageEncode.worker`` and multiplexes requests onto
|
||||
* it by a random request id. Falls back to an inline call when the Worker
|
||||
* can't be constructed (tests, ancient browsers) so the Composer always has a
|
||||
* working path.
|
||||
*/
|
||||
import {
|
||||
encodeImageInWorker,
|
||||
type EncodeResponse,
|
||||
} from "@/workers/imageEncode.worker";
|
||||
|
||||
export type { EncodeResponse, EncodeSuccess, EncodeFailure } from "@/workers/imageEncode.worker";
|
||||
export { TARGET_MAX_BYTES } from "@/workers/imageEncode.worker";
|
||||
|
||||
type Pending = {
|
||||
resolve: (r: EncodeResponse) => void;
|
||||
reject: (err: Error) => void;
|
||||
};
|
||||
|
||||
let worker: Worker | null = null;
|
||||
let bootAttempted = false;
|
||||
const pending = new Map<string, Pending>();
|
||||
|
||||
function bootWorker(): Worker | null {
|
||||
if (bootAttempted) return worker;
|
||||
bootAttempted = true;
|
||||
if (typeof Worker === "undefined") return null;
|
||||
try {
|
||||
worker = new Worker(
|
||||
new URL("@/workers/imageEncode.worker.ts", import.meta.url),
|
||||
{ type: "module" },
|
||||
);
|
||||
worker.addEventListener("message", (ev: MessageEvent<EncodeResponse>) => {
|
||||
const entry = pending.get(ev.data.id);
|
||||
if (!entry) return;
|
||||
pending.delete(ev.data.id);
|
||||
entry.resolve(ev.data);
|
||||
});
|
||||
worker.addEventListener("error", (ev) => {
|
||||
// Cancel every in-flight request on a Worker crash.
|
||||
for (const [, entry] of pending) {
|
||||
entry.reject(new Error(`image encoder worker error: ${ev.message}`));
|
||||
}
|
||||
pending.clear();
|
||||
worker?.terminate();
|
||||
worker = null;
|
||||
});
|
||||
return worker;
|
||||
} catch {
|
||||
worker = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function newId(): string {
|
||||
// ``crypto.randomUUID`` is widely available; fall back to Math.random if not.
|
||||
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
||||
return (crypto as Crypto).randomUUID();
|
||||
}
|
||||
return `img-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
/** Encode ``file`` off the main thread when possible. Always resolves — errors
|
||||
* are returned as ``{ok: false, reason}`` — so callers can render inline
|
||||
* validation without wrapping in try/catch. */
|
||||
export async function encodeImage(file: File): Promise<EncodeResponse> {
|
||||
const id = newId();
|
||||
const w = bootWorker();
|
||||
if (!w) {
|
||||
// Inline fallback: same logic, just on the main thread.
|
||||
return encodeImageInWorker({ id, file });
|
||||
}
|
||||
return new Promise<EncodeResponse>((resolve, reject) => {
|
||||
pending.set(id, { resolve, reject });
|
||||
try {
|
||||
w.postMessage({ id, file });
|
||||
} catch (err) {
|
||||
pending.delete(id);
|
||||
reject(err instanceof Error ? err : new Error(String(err)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Release the singleton Worker (tests / teardown). */
|
||||
export function disposeImageEncoder(): void {
|
||||
if (worker) {
|
||||
worker.terminate();
|
||||
worker = null;
|
||||
}
|
||||
bootAttempted = false;
|
||||
for (const [, entry] of pending) {
|
||||
entry.reject(new Error("image encoder disposed"));
|
||||
}
|
||||
pending.clear();
|
||||
}
|
||||
Reference in New Issue
Block a user