Add ExifTool worker and Pinia store for off-thread metadata reads.
Warm up WASM in a dedicated worker with a tiny JPEG probe and expose init/read RPC to the main thread.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { parseMetadata } from '@uswriting/exiftool';
|
||||
import { EXIFTOOL_WORKER_INIT_JPEG } from '@/consts/exiftoolWorkerInitJpeg';
|
||||
import type { ExiftoolWorkerInMessage } from '@/types/workers/exiftoolWorkerInMessage';
|
||||
import { resolveErrorMessage } from '@/utils/resolveErrorMessage';
|
||||
|
||||
let initialized = false;
|
||||
let initializeWorkerPromise: Promise<void> | null = null;
|
||||
|
||||
/**
|
||||
* Cold-start the ExifTool WASM stack inside this worker (once per worker lifetime).
|
||||
*
|
||||
* `@uswriting/exiftool` has no public `init()` API: the first `parseMetadata` / `writeMetadata`
|
||||
* call downloads `zeroperl.wasm`, boots Perl, and mounts the bundled ExifTool script. To init
|
||||
* before the user picks a file, we must trigger that path ourselves.
|
||||
*
|
||||
* We run `parseMetadata` on {@link EXIFTOOL_WORKER_INIT_JPEG} so the library performs
|
||||
* a real read without needing a user `File`. Output is discarded; sets {@link initialized}.
|
||||
*
|
||||
* Concurrent callers (init message, read-metadata, future write) share one in-flight warmup.
|
||||
*
|
||||
* @throws When warmup `parseMetadata` fails (network, WASM, or ExifTool error).
|
||||
* @see Main thread `initialize()` posts `{ type: 'init' }` when idle.
|
||||
*/
|
||||
const initializeWorker = async (): Promise<void> => {
|
||||
if (initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!initializeWorkerPromise) {
|
||||
initializeWorkerPromise = (async () => {
|
||||
try {
|
||||
const result = await parseMetadata({ name: 'init.jpg', data: EXIFTOOL_WORKER_INIT_JPEG });
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
} catch (error) {
|
||||
initializeWorkerPromise = null;
|
||||
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return initializeWorkerPromise;
|
||||
};
|
||||
|
||||
self.addEventListener('message', async (event: MessageEvent<ExiftoolWorkerInMessage>) => {
|
||||
const message = event.data;
|
||||
|
||||
if (message.type === 'init') {
|
||||
try {
|
||||
await initializeWorker();
|
||||
|
||||
self.postMessage({ type: 'init-done' });
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
type: 'init-error',
|
||||
message: resolveErrorMessage(error, 'Failed to prepare ExifTool')
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'read-metadata') {
|
||||
try {
|
||||
await initializeWorker();
|
||||
|
||||
const result = await parseMetadata(message.file);
|
||||
|
||||
if (!result.success) {
|
||||
self.postMessage({ type: 'read-metadata-error', id: message.id, message: result.error });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
self.postMessage({ type: 'read-metadata-done', id: message.id, output: result.data });
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
type: 'read-metadata-error',
|
||||
id: message.id,
|
||||
message: resolveErrorMessage(error, 'Could not read image metadata')
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user