From f1e5c980130359e3122c2318c140c12db64b190a Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Tue, 8 Sep 2026 17:41:27 -0400 Subject: [PATCH] feat: add image derivative worker lifecycle Add worker-owned image derivative job execution with explicit PROCESSING, COMPLETED, and FAILED outcomes, plus tests and documentation. Verification: npm test passed 70 tests, worker tests passed 2 tests, and integration passed 14 tests. No build script is configured. --- apps/worker/src/image-derivative-worker.js | 26 ++++++++++++ .../tests/image-derivative-worker.test.js | 40 +++++++++++++++++++ docs/IMAGES.md | 4 ++ docs/IMPLEMENTATION-STATUS.md | 5 ++- 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 apps/worker/src/image-derivative-worker.js create mode 100644 apps/worker/tests/image-derivative-worker.test.js diff --git a/apps/worker/src/image-derivative-worker.js b/apps/worker/src/image-derivative-worker.js new file mode 100644 index 0000000..178d5d8 --- /dev/null +++ b/apps/worker/src/image-derivative-worker.js @@ -0,0 +1,26 @@ +export async function runImageDerivativeJob({ job, loadSource, processor }) { + if (!job || job.type !== 'IMAGE_DERIVATIVES') { + throw new Error('Unsupported image derivative job.'); + } + + const processingJob = { ...job, status: 'PROCESSING', error: null }; + + try { + const source = await loadSource(job.sourceKey); + const derivatives = await processor.process(source); + + return { + ...processingJob, + status: 'COMPLETED', + derivatives, + completedAt: new Date(), + }; + } catch (error) { + return { + ...processingJob, + status: 'FAILED', + error: error instanceof Error ? error.message : 'Image derivative processing failed.', + failedAt: new Date(), + }; + } +} \ No newline at end of file diff --git a/apps/worker/tests/image-derivative-worker.test.js b/apps/worker/tests/image-derivative-worker.test.js new file mode 100644 index 0000000..196c4dd --- /dev/null +++ b/apps/worker/tests/image-derivative-worker.test.js @@ -0,0 +1,40 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { runImageDerivativeJob } from '../src/image-derivative-worker.js'; + +test('completes an image derivative job after loading and processing the source', async () => { + const calls = []; + const result = await runImageDerivativeJob({ + job: { id: 'job-1', type: 'IMAGE_DERIVATIVES', sourceKey: 'source.jpg', status: 'PENDING' }, + async loadSource(sourceKey) { + calls.push(`load:${sourceKey}`); + return { sourceKey, data: Buffer.from('image') }; + }, + processor: { + async process(source) { + calls.push(`process:${source.sourceKey}`); + return [{ name: 'thumbnail', key: 'source-thumbnail.webp' }]; + }, + }, + }); + + assert.equal(result.status, 'COMPLETED'); + assert.deepEqual(result.derivatives, [{ name: 'thumbnail', key: 'source-thumbnail.webp' }]); + assert.deepEqual(calls, ['load:source.jpg', 'process:source.jpg']); + assert.ok(result.completedAt instanceof Date); +}); + +test('records a failed image derivative job without throwing worker errors', async () => { + const result = await runImageDerivativeJob({ + job: { id: 'job-2', type: 'IMAGE_DERIVATIVES', sourceKey: 'missing.jpg', status: 'PENDING' }, + async loadSource() { + throw new Error('source unavailable'); + }, + processor: { async process() { return []; } }, + }); + + assert.equal(result.status, 'FAILED'); + assert.equal(result.error, 'source unavailable'); + assert.ok(result.failedAt instanceof Date); +}); \ No newline at end of file diff --git a/docs/IMAGES.md b/docs/IMAGES.md index 9a59bd7..49920ea 100644 --- a/docs/IMAGES.md +++ b/docs/IMAGES.md @@ -73,6 +73,10 @@ variants and writes them through `ImageStorage` using an injected renderer. The concrete decoder/encoder and worker queue remain separate infrastructure work so local development does not depend on a machine-specific binary. +The worker now owns the derivative job lifecycle and reports explicit +`PENDING`, `PROCESSING`, `COMPLETED`, or `FAILED` outcomes. Queue transport and +persistent job records remain deployment-specific work. + ## Metadata Store: diff --git a/docs/IMPLEMENTATION-STATUS.md b/docs/IMPLEMENTATION-STATUS.md index d5aff5e..08b9295 100644 --- a/docs/IMPLEMENTATION-STATUS.md +++ b/docs/IMPLEMENTATION-STATUS.md @@ -92,6 +92,7 @@ docker context use orbstack - Item-owned image metadata can be added through a seller-scoped, CSRF-protected admin route; publication uses the persisted image count. - `ImageStorage`, traversal-safe local storage, multipart upload transport, and binary signature validation are implemented; derivative processing remains. - Derivative orchestration now defines thumbnail, medium, and large WebP storage outputs behind an injected renderer; the concrete worker-backed renderer remains. +- The worker owns an image derivative job runner with explicit processing, completion, and failure outcomes; queue transport and persistent job records remain. - Image metadata limits are enforced in the service boundary: supported MIME types, 10 MiB default file size, and 10,000-pixel maximum dimensions. ### Verified Baseline @@ -103,7 +104,7 @@ docker context use orbstack | HTTP | Product and inventory creation, validation, authentication/recovery, CSRF, rate limiting, and session management | | Integration | PostgreSQL catalog/session/audit/password recovery persistence and Redis-shared login throttling | -Latest result: `npm test` passed 70 tests; `npm run test:integration` passed 14 tests against OrbStack PostgreSQL and Redis. +Latest result: `npm test` passed 70 tests; worker tests passed 2 tests; `npm run test:integration` passed 14 tests against OrbStack PostgreSQL and Redis. ## Next Milestone @@ -132,7 +133,7 @@ Work through one bounded vertical slice at a time. The linked documentation rema - No initial User/Seller is provisioned in the development database yet. Run `npm run provision:admin` after setting its required local `.env` variables. - Listings are partially implemented: draft creation, seller-scoped reads, publication gating, lifecycle writes, archive behavior, sold/archived inventory checks, and item-owned image metadata are complete; storage-backed processing remains. -- Sales, acquisitions, pricing, image processing, search, worker jobs, web UI, CI, and deployment services remain unimplemented. +- Sales, acquisitions, pricing, concrete image rendering, search, queue transport, persistent worker jobs, web UI, CI, and deployment services remain unimplemented. - `npm install` currently reports dependency audit findings. Review and remediate them before production deployment; do not use `npm audit fix --force` without reviewing its changes. ## Local Commands