Slave/metadata pipeline #6
@@ -0,0 +1,3 @@
|
|||||||
|
export type ExiftoolWorkerJob =
|
||||||
|
| { type: 'init' }
|
||||||
|
| { type: 'read-metadata'; id: string; file: File };
|
||||||
@@ -2,11 +2,15 @@ import zeroperlWasmUrl from '@6over3/zeroperl-ts/zeroperl.wasm?url';
|
|||||||
import { parseMetadata } from '@uswriting/exiftool';
|
import { parseMetadata } from '@uswriting/exiftool';
|
||||||
import { EXIFTOOL_WORKER_INIT_JPEG } from '@/consts/exiftoolWorkerInitJpeg';
|
import { EXIFTOOL_WORKER_INIT_JPEG } from '@/consts/exiftoolWorkerInitJpeg';
|
||||||
import type { ExiftoolWorkerInMessage } from '@/types/workers/exiftoolWorkerInMessage';
|
import type { ExiftoolWorkerInMessage } from '@/types/workers/exiftoolWorkerInMessage';
|
||||||
|
import type { ExiftoolWorkerJob } from '@/types/workers/exiftoolWorkerJob';
|
||||||
import { resolveErrorMessage } from '@/utils/resolveErrorMessage';
|
import { resolveErrorMessage } from '@/utils/resolveErrorMessage';
|
||||||
|
|
||||||
/** Custom fetch function for `@uswriting/exiftool` to load `zeroperl.wasm` from the worker. */
|
/** Custom fetch function for `@uswriting/exiftool` to load `zeroperl.wasm` from the worker. */
|
||||||
const exiftoolParseOptions = { fetch: () => fetch(zeroperlWasmUrl) };
|
const exiftoolParseOptions = { fetch: () => fetch(zeroperlWasmUrl) };
|
||||||
|
|
||||||
|
const jobQueue: ExiftoolWorkerJob[] = [];
|
||||||
|
let drainPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
let initializeWorkerPromise: Promise<void> | null = null;
|
let initializeWorkerPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
@@ -20,8 +24,6 @@ let initializeWorkerPromise: Promise<void> | null = null;
|
|||||||
* We run `parseMetadata` on {@link EXIFTOOL_WORKER_INIT_JPEG} so the library performs
|
* 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}.
|
* 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).
|
* @throws When warmup `parseMetadata` fails (network, WASM, or ExifTool error).
|
||||||
* @see Main thread `initializeExiftool()` posts `{ type: 'init' }` when idle.
|
* @see Main thread `initializeExiftool()` posts `{ type: 'init' }` when idle.
|
||||||
*/
|
*/
|
||||||
@@ -54,10 +56,36 @@ const initializeWorker = async (): Promise<void> => {
|
|||||||
return initializeWorkerPromise;
|
return initializeWorkerPromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.addEventListener('message', async (event: MessageEvent<ExiftoolWorkerInMessage>) => {
|
const enqueue = (job: ExiftoolWorkerJob): void => {
|
||||||
const message = event.data;
|
jobQueue.push(job);
|
||||||
|
|
||||||
if (message.type === 'init') {
|
if (!drainPromise) {
|
||||||
|
drainPromise = (async () => {
|
||||||
|
try {
|
||||||
|
await drain();
|
||||||
|
} catch {
|
||||||
|
console.error('Failed to drain ExifTool worker');
|
||||||
|
}
|
||||||
|
|
||||||
|
drainPromise = null;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const drain = async (): Promise<void> => {
|
||||||
|
while (jobQueue.length > 0) {
|
||||||
|
const job = jobQueue.shift();
|
||||||
|
|
||||||
|
if (!job) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await processJob(job);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processJob = async (job: ExiftoolWorkerJob): Promise<void> => {
|
||||||
|
if (job.type === 'init') {
|
||||||
try {
|
try {
|
||||||
await initializeWorker();
|
await initializeWorker();
|
||||||
|
|
||||||
@@ -72,25 +100,39 @@ self.addEventListener('message', async (event: MessageEvent<ExiftoolWorkerInMess
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === 'read-metadata') {
|
if (job.type === 'read-metadata') {
|
||||||
try {
|
try {
|
||||||
await initializeWorker();
|
await initializeWorker();
|
||||||
|
|
||||||
const result = await parseMetadata(message.file, exiftoolParseOptions);
|
const result = await parseMetadata(job.file, exiftoolParseOptions);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
self.postMessage({ type: 'read-metadata-error', id: message.id, message: result.error });
|
self.postMessage({ type: 'read-metadata-error', id: job.id, message: result.error });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.postMessage({ type: 'read-metadata-done', id: message.id, output: result.data });
|
self.postMessage({ type: 'read-metadata-done', id: job.id, output: result.data });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: 'read-metadata-error',
|
type: 'read-metadata-error',
|
||||||
id: message.id,
|
id: job.id,
|
||||||
message: resolveErrorMessage(error, 'Could not read image metadata')
|
message: resolveErrorMessage(error, 'Could not read image metadata')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.addEventListener('message', (event: MessageEvent<ExiftoolWorkerInMessage>) => {
|
||||||
|
const message = event.data;
|
||||||
|
|
||||||
|
if (message.type === 'init') {
|
||||||
|
enqueue({ type: 'init' });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === 'read-metadata') {
|
||||||
|
enqueue({ type: 'read-metadata', id: message.id, file: message.file });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user