This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@pokemon-inventory/worker",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node src/worker-runtime.js",
|
||||
"test": "node --test tests/*.test.js"
|
||||
}
|
||||
}
|
||||
@@ -110,4 +110,21 @@ export async function runImageDerivativeJob({ job, loadSource, processor }) {
|
||||
failedAt: new Date(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function processNextPersistentImageDerivativeJob({ jobRepository, loadSource, processor }) {
|
||||
const claimed = await jobRepository.claimNext();
|
||||
|
||||
if (!claimed) return null;
|
||||
|
||||
const job = { ...claimed, type: 'IMAGE_DERIVATIVES' };
|
||||
const result = await runImageDerivativeJob({ job, loadSource, processor });
|
||||
|
||||
if (result.status === 'COMPLETED') {
|
||||
await jobRepository.complete(claimed.id, result.derivatives);
|
||||
} else {
|
||||
await jobRepository.fail(claimed.id, result.error);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import dotenv from 'dotenv';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
dotenv.config({ path: fileURLToPath(new URL('../../../.env', import.meta.url)) });
|
||||
|
||||
import { createDatabase } from '../../api/src/infrastructure/database/client.js';
|
||||
import { createLocalImageStorage } from '../../api/src/infrastructure/images/local-image-storage.js';
|
||||
import { createImageDerivativeProcessor } from '../../api/src/infrastructure/images/image-derivative-processor.js';
|
||||
import { createSharpImageRenderer } from '../../api/src/infrastructure/images/sharp-image-renderer.js';
|
||||
import { createImageDerivativeJobRepository } from '../../api/src/modules/inventory/repositories/image-derivative-job-repository.js';
|
||||
import { processNextPersistentImageDerivativeJob } from './image-derivative-worker.js';
|
||||
|
||||
const pollIntervalMs = Number.parseInt(process.env.WORKER_POLL_INTERVAL_MS ?? '2000', 10);
|
||||
|
||||
async function runWorker() {
|
||||
const database = createDatabase();
|
||||
const imageStorage = createLocalImageStorage(process.env.IMAGE_STORAGE_ROOT ?? '.local/image-storage');
|
||||
const jobRepository = createImageDerivativeJobRepository(database.db);
|
||||
const processor = createImageDerivativeProcessor({
|
||||
imageStorage,
|
||||
renderer: createSharpImageRenderer(),
|
||||
});
|
||||
|
||||
async function processOne() {
|
||||
const result = await processNextPersistentImageDerivativeJob({
|
||||
jobRepository,
|
||||
async loadSource(sourceKey) {
|
||||
return { sourceKey, data: await imageStorage.get(sourceKey) };
|
||||
},
|
||||
processor,
|
||||
});
|
||||
|
||||
if (result) console.log(`Image derivative job ${result.id} ${result.status.toLowerCase()}.`);
|
||||
}
|
||||
|
||||
await processOne();
|
||||
const timer = setInterval(() => processOne().catch((error) => console.error(error)), pollIntervalMs);
|
||||
|
||||
const shutdown = async () => {
|
||||
clearInterval(timer);
|
||||
await database.close();
|
||||
};
|
||||
process.once('SIGINT', shutdown);
|
||||
process.once('SIGTERM', shutdown);
|
||||
}
|
||||
|
||||
runWorker().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import test from 'node:test';
|
||||
import {
|
||||
createInMemoryImageDerivativeQueue,
|
||||
processNextImageDerivativeJob,
|
||||
processNextPersistentImageDerivativeJob,
|
||||
runImageDerivativeJob,
|
||||
} from '../src/image-derivative-worker.js';
|
||||
|
||||
@@ -66,4 +67,32 @@ test('claims queued jobs and persists their processing lifecycle', async () => {
|
||||
assert.equal(result.status, 'COMPLETED');
|
||||
assert.equal(queue.getJob('job-3').status, 'COMPLETED');
|
||||
assert.equal(queue.getJob('job-3').derivatives[0].key, 'queued.jpg-thumbnail.webp');
|
||||
});
|
||||
|
||||
test('claims and persists a durable image derivative job lifecycle', async () => {
|
||||
const calls = [];
|
||||
const jobRepository = {
|
||||
async claimNext() {
|
||||
return { id: 'job-4', sourceKey: 'durable.jpg', status: 'PROCESSING' };
|
||||
},
|
||||
async complete(id, derivatives) {
|
||||
calls.push({ action: 'complete', id, derivatives });
|
||||
},
|
||||
async fail() {
|
||||
throw new Error('A successful durable job must not fail.');
|
||||
},
|
||||
};
|
||||
|
||||
const result = await processNextPersistentImageDerivativeJob({
|
||||
jobRepository,
|
||||
async loadSource(sourceKey) { return { sourceKey, data: Buffer.from('image') }; },
|
||||
processor: { async process() { return [{ name: 'thumbnail', key: 'durable-thumbnail.webp' }]; } },
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'COMPLETED');
|
||||
assert.deepEqual(calls, [{
|
||||
action: 'complete',
|
||||
id: 'job-4',
|
||||
derivatives: [{ name: 'thumbnail', key: 'durable-thumbnail.webp' }],
|
||||
}]);
|
||||
});
|
||||
Reference in New Issue
Block a user