Extract product sync queue db calls into Queue Service methods

This commit is contained in:
Dominic Ferrando
2026-07-11 15:42:16 -04:00
parent a09d11e8e4
commit da3c9e588e
2 changed files with 47 additions and 57 deletions
+46 -56
View File
@@ -5,9 +5,9 @@ import { DatabaseError } from "pg";
import type { Insertable, Selectable } from "kysely";
import type { ProductSyncs } from "../database/out/db";
type SyncStatus = "queued" | "active" | "failed" | "succeeded";
type SyncStatus = "queued" | "active" | "failed" | "fulfilled";
type QueuedProductSync = {
type ProductSyncPayload = {
id?: string,
session: {
name: string,
@@ -40,38 +40,15 @@ class ProductSyncService {
this.ProductSyncer = new ProductSyncer(this.Printful, this.Webflow, log);
}
async start(queuedProductSync: QueuedProductSync) {
let id = queuedProductSync.id ?? null;
async start(payload: ProductSyncPayload) {
let id = payload.id ?? null;
let freedSlot = false;
try {
log.info({ session: queuedProductSync.session }, "starting sync run");
log.info({ session: payload.session }, "starting sync run");
await this.ProductSyncer.run({
filter: queuedProductSync.filter,
filter: payload.filter,
onStart: async (startDate) => {
const values: Insertable<ProductSyncs> = {
session_id: queuedProductSync.session.id,
session_name: queuedProductSync.session.name,
started_at: startDate,
p_product_id_filter: queuedProductSync.filter?.pProductIds ?? null,
};
if (id) {
const result = await db.updateTable("product_syncs")
.set(values)
.where("id", "=", id)
.where("started_at", "is", null)
.executeTakeFirst();
// Guard against another worker/machine having already claimed
// this queued row between `queue.front()` and now.
if (result.numUpdatedRows === 0n) throw new AlreadyClaimedError();
}
else {
const result = await db.insertInto("product_syncs")
.values(values)
.returning("id")
.executeTakeFirstOrThrow();
id = result.id;
}
await this.Queue.enqueue(payload, startDate);
},
onStep: async (pProductIds: number[]) => {
if (!id) throw new Error("Expected sync run ID");
@@ -82,10 +59,7 @@ class ProductSyncService {
},
onCompletion: async (completionDate, duration) => {
if (!id) throw new Error("Expected sync run ID");
await db.updateTable("product_syncs")
.set({ syncing_p_product_ids: [], ended_at: completionDate })
.where("id", "=", id)
.execute();
await this.Queue.fulfill(id, completionDate)
freedSlot = true;
}
});
@@ -103,23 +77,17 @@ class ProductSyncService {
// If active sync has been syncing for 10 min (600000ms), clear it and retry this sync
if (Date.now() - (activeSync.started_at?.getTime() ?? 0) > 600000) {
log.info("already active sync exceeded timeout");
await db.updateTable("product_syncs")
.set({ syncing_p_product_ids: [], ended_at: new Date(), has_failed: true })
.where("id", "=", activeSync.id)
.execute();
await this.start(queuedProductSync);
await this.Queue.fail(activeSync.id, new Date());
await this.start(payload);
}
else {
log.info("sync already active, enqueuing next sync");
await this.Queue.enqueue(queuedProductSync);
await this.Queue.enqueue(payload);
}
}
else {
if (id) {
await db.updateTable("product_syncs")
.set({ syncing_p_product_ids: [], ended_at: new Date(), has_failed: true })
.where("id", "=", id)
.execute();
await this.Queue.fail(id, new Date());
freedSlot = true;
}
throw err;
@@ -164,31 +132,39 @@ class ProductSyncService {
deriveStatus(sync: Selectable<ProductSyncs>): SyncStatus {
if (!sync.started_at) return "queued";
if (!sync.ended_at) return "active";
return sync.has_failed ? "failed" : "succeeded";
return sync.has_failed ? "failed" : "fulfilled";
}
}
class ProductSyncQueueService {
async enqueue(queuedProductSync: QueuedProductSync) {
async enqueue(payload: ProductSyncPayload, startDate?: Date): Promise<string> {
const values: Insertable<ProductSyncs> = {
session_id: queuedProductSync.session.id,
session_name: queuedProductSync.session.name,
p_product_id_filter: queuedProductSync.filter?.pProductIds ?? null
session_id: payload.session.id,
session_name: payload.session.name,
p_product_id_filter: payload.filter?.pProductIds ?? null
};
if (queuedProductSync.id) {
await db.updateTable("product_syncs")
if (startDate) values.started_at = startDate;
if (payload.id) {
const result = await db.updateTable("product_syncs")
.set(values)
.where("id", "=", queuedProductSync.id)
.execute();
.where("id", "=", payload.id)
.where("started_at", "is", null)
.executeTakeFirstOrThrow();
// Guard against another worker/machine having already claimed
// this queued row between `queue.front()` and now.
if (result.numUpdatedRows === 0n) throw new AlreadyClaimedError();
return payload.id;
}
else {
await db.insertInto("product_syncs")
const result = await db.insertInto("product_syncs")
.values(values)
.execute();
.returning("id")
.executeTakeFirstOrThrow();
return result.id;
}
}
async front(): Promise<QueuedProductSync | undefined> {
async front(): Promise<ProductSyncPayload | undefined> {
const result = await db.selectFrom("product_syncs")
.selectAll()
.where("started_at", "is", null)
@@ -208,4 +184,18 @@ class ProductSyncQueueService {
}
}
}
async fail(id: string, endDate: Date) {
await db.updateTable("product_syncs")
.set({ syncing_p_product_ids: [], ended_at: endDate, has_failed: true })
.where("id", "=", id)
.execute();
}
async fulfill(id: string, endDate: Date) {
await db.updateTable("product_syncs")
.set({ syncing_p_product_ids: [], ended_at: endDate })
.where("id", "=", id)
.execute();
}
}
@@ -3,7 +3,7 @@
import { onMount } from "svelte";
import { api } from "$lib/api.js";
type SyncStatus = "queued" | "active" | "failed" | "succeeded" | "none";
type SyncStatus = "queued" | "active" | "failed" | "fulfilled" | "none";
const { data } = $props();