Files
private_image/frontend/src/stores/exiftool.ts
T
nobswebdev b47c5c7696 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.
2026-10-15 17:18:21 +02:00

127 lines
3.7 KiB
TypeScript

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
};
});