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
+101
View File
@@ -60,6 +60,107 @@ export class FakeRecipeRepository {
}
}
export class FakeImportJobRepository {
constructor() {
this.jobs = new Map();
this.nextId = 1;
}
async enqueue({ ownerGoogleSub, url, platform, sourceId }) {
const existing = [...this.jobs.values()].find((job) => (
job.ownerGoogleSub === ownerGoogleSub
&& job.platform === platform
&& job.sourceId === sourceId
));
if (existing) return { created: false, job: existing };
const now = new Date();
const job = {
_id: `job-${this.nextId}`,
ownerGoogleSub,
url,
platform,
sourceId,
status: 'pending',
attempts: 0,
error: null,
recipeId: null,
createdAt: now,
updatedAt: now,
startedAt: null,
completedAt: null,
lockedAt: null,
};
this.nextId += 1;
this.jobs.set(job._id, job);
return { created: true, job };
}
async listVisible(ownerGoogleSub) {
return [...this.jobs.values()].filter((job) => (
job.ownerGoogleSub === ownerGoogleSub
&& ['pending', 'processing', 'failed'].includes(job.status)
));
}
async recoverStale(ownerGoogleSub, staleBefore) {
for (const job of this.jobs.values()) {
if (
job.ownerGoogleSub === ownerGoogleSub
&& job.status === 'processing'
&& job.lockedAt < staleBefore
) {
Object.assign(job, { status: 'pending', startedAt: null, lockedAt: null });
}
}
}
async claimNext(ownerGoogleSub) {
const job = [...this.jobs.values()].find((candidate) => (
candidate.ownerGoogleSub === ownerGoogleSub && candidate.status === 'pending'
));
if (!job) return null;
const now = new Date();
Object.assign(job, {
status: 'processing',
error: null,
attempts: job.attempts + 1,
startedAt: now,
lockedAt: now,
updatedAt: now,
});
return job;
}
async markCompleted(jobId, recipeId) {
const job = this.jobs.get(jobId);
Object.assign(job, {
status: 'completed',
recipeId,
error: null,
completedAt: new Date(),
lockedAt: null,
});
}
async markFailed(jobId, error) {
Object.assign(this.jobs.get(jobId), { status: 'failed', error, lockedAt: null });
}
async retry(ownerGoogleSub, jobId) {
const job = this.jobs.get(jobId);
if (job?.ownerGoogleSub !== ownerGoogleSub || job.status !== 'failed') return null;
Object.assign(job, {
status: 'pending',
error: null,
startedAt: null,
lockedAt: null,
updatedAt: new Date(),
});
return job;
}
}
export class FakeGroupRepository {
constructor(groups = []) {
this.groups = groups;