Warm up WASM in a dedicated worker with a tiny JPEG probe and expose init/read RPC to the main thread.
127 lines
3.7 KiB
TypeScript
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
|
|
};
|
|
});
|