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,126 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import type { ExiftoolInitStatus } from '@/types/metadata/ExiftoolInitStatus';
|
||||
import type { ExiftoolWorkerOutMessage } from '@/types/workers/exiftoolWorkerOutMessage';
|
||||
|
||||
export const useExiftoolStore = defineStore('exiftool', () => {
|
||||
const initStatus = ref<ExiftoolInitStatus>('idle');
|
||||
const initErrorMessage = ref<string | null>(null);
|
||||
|
||||
const worker = ref<Worker | null>(null);
|
||||
|
||||
let initPromise: Promise<void> | null = null;
|
||||
let initPromiseResolve: (() => void) | null = null;
|
||||
let initPromiseReject: ((error: Error) => void) | null = null;
|
||||
|
||||
const readMetadataWaiters = new Map<
|
||||
string,
|
||||
{ resolve: (output: string) => void; reject: (error: Error) => void }
|
||||
>();
|
||||
|
||||
const initializeExiftool = async (): Promise<void> => {
|
||||
if (initStatus.value === 'ready') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (initStatus.value === 'error') {
|
||||
throw new Error(initErrorMessage.value ?? 'ExifTool is unavailable');
|
||||
}
|
||||
|
||||
if (initStatus.value === 'idle') {
|
||||
initStatus.value = 'loading';
|
||||
initErrorMessage.value = null;
|
||||
initPromise = new Promise<void>((resolve, reject) => {
|
||||
initPromiseResolve = resolve;
|
||||
initPromiseReject = reject;
|
||||
});
|
||||
|
||||
if (!worker.value) {
|
||||
worker.value = new Worker(new URL('../workers/exiftoolWorker.ts', import.meta.url), {
|
||||
type: 'module'
|
||||
});
|
||||
|
||||
worker.value.addEventListener('message', handleWorkerMessage);
|
||||
}
|
||||
|
||||
worker.value.postMessage({ type: 'init' });
|
||||
}
|
||||
|
||||
if (!initPromise) {
|
||||
throw new Error('ExifTool is not initializing');
|
||||
}
|
||||
|
||||
await initPromise;
|
||||
};
|
||||
|
||||
const handleWorkerMessage = (event: MessageEvent<ExiftoolWorkerOutMessage>): void => {
|
||||
const message = event.data;
|
||||
|
||||
if (message.type === 'init-done') {
|
||||
initStatus.value = 'ready';
|
||||
initPromiseResolve?.();
|
||||
initPromiseResolve = null;
|
||||
initPromiseReject = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'init-error') {
|
||||
initStatus.value = 'error';
|
||||
initErrorMessage.value = message.message;
|
||||
initPromiseReject?.(new Error(message.message));
|
||||
initPromiseResolve = null;
|
||||
initPromiseReject = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'read-metadata-done') {
|
||||
const waiter = readMetadataWaiters.get(message.id);
|
||||
|
||||
if (!waiter) {
|
||||
return;
|
||||
}
|
||||
|
||||
readMetadataWaiters.delete(message.id);
|
||||
waiter.resolve(message.output);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'read-metadata-error') {
|
||||
const waiter = readMetadataWaiters.get(message.id);
|
||||
|
||||
if (!waiter) {
|
||||
return;
|
||||
}
|
||||
|
||||
readMetadataWaiters.delete(message.id);
|
||||
waiter.reject(new Error(message.message));
|
||||
}
|
||||
};
|
||||
|
||||
const readMetadata = async (file: File): Promise<string> => {
|
||||
await initializeExiftool();
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
const workerInstance = worker.value;
|
||||
|
||||
if (!workerInstance) {
|
||||
throw new Error('ExifTool worker is unavailable');
|
||||
}
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
readMetadataWaiters.set(id, { resolve, reject });
|
||||
workerInstance.postMessage({ type: 'read-metadata', id, file });
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
initStatus,
|
||||
initErrorMessage,
|
||||
initializeExiftool,
|
||||
readMetadata
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user