feat: add batched recipe import queue

This commit is contained in:
2026-08-28 11:09:36 +09:00
parent 4491c91455
commit f65e660184
19 changed files with 1080 additions and 262 deletions
+76
View File
@@ -0,0 +1,76 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
createImportBatches,
ImportJobProcessorService,
} from '../src/services/import-job-processor.service.js';
import { FakeImportJobRepository, recipeDraft, source } from './helpers/fakes.js';
function extractedItem(jobId, rawText) {
return {
jobId,
source: { rawText },
};
}
test('가져오기 본문은 최대 3개와 합계 100,000자 단위로 묶는다', () => {
const batches = createImportBatches([
extractedItem('job-1', 'a'.repeat(40_000)),
extractedItem('job-2', 'b'.repeat(40_000)),
extractedItem('job-3', 'c'.repeat(30_000)),
extractedItem('job-4', 'd'),
extractedItem('job-5', 'e'),
]);
assert.deepEqual(batches.map((batch) => batch.map(({ jobId }) => jobId)), [
['job-1', 'job-2'],
['job-3', 'job-4', 'job-5'],
]);
});
test('처리 요청 하나가 대기열 전체를 3개씩 MiniMax 배치 처리한다', async () => {
const importJobRepository = new FakeImportJobRepository();
for (let index = 1; index <= 4; index += 1) {
await importJobRepository.enqueue({
ownerGoogleSub: 'google-user-1',
url: `https://www.instagram.com/reel/Recipe${index}/`,
platform: 'instagram',
sourceId: `Recipe${index}`,
});
}
const batchJobIds = [];
const processor = new ImportJobProcessorService({
importJobRepository,
sourceExtractor: {
async extract(url) {
const sourceId = new URL(url).pathname.split('/').filter(Boolean)[1];
return {
...source,
sourceUrl: url,
sourceId,
rawText: `${sourceId} 본문`,
};
},
},
recipeParser: {
async parseMany(entries) {
batchJobIds.push(entries.map(({ jobId }) => jobId));
return entries.map(({ jobId }) => ({ jobId, recipe: recipeDraft }));
},
},
recipeCreator: {
async create({ source: recipeSource }) {
return { _id: `recipe-${recipeSource.sourceId}` };
},
},
logger: { error() {} },
});
processor.start('google-user-1');
await processor.waitForIdle('google-user-1');
assert.deepEqual(batchJobIds.map((ids) => ids.length), [3, 1]);
assert.deepEqual(await importJobRepository.listVisible('google-user-1'), []);
assert.ok([...importJobRepository.jobs.values()].every(({ status }) => status === 'completed'));
});