Implement sync service queue

This commit is contained in:
Dominic Ferrando
2026-07-03 14:07:47 -04:00
parent fc9cd6f1ae
commit 437300831f
4 changed files with 99 additions and 37 deletions
+81 -18
View File
@@ -1,29 +1,72 @@
import { PrintfulClient, ProductSyncer, WebflowClient, type SyncOptions } from "@blade-and-brawn/commerce";
import { PrintfulClient, ProductSyncer, WebflowClient, type ProductSyncerOptions } from "@blade-and-brawn/commerce";
import { log } from "../util";
import { db } from "../database/db";
import { DatabaseError } from "pg";
class SyncQueue {
async enqueue(item: SyncServiceOptions) {
const values = { printful_product_id_filter: item.filter?.printfulProductIds };
if (item.id) {
await db.updateTable("product_syncs")
.set(values)
.where("id", "=", item.id)
.execute();
}
else {
await db.insertInto("product_syncs")
.values(values)
.execute();
}
}
async front() {
return await db.selectFrom("product_syncs")
.selectAll()
.where("started_at", "is", null)
.orderBy("created_at", "asc")
.limit(1)
.executeTakeFirst();
}
}
type SyncServiceOptions = {
id?: string | null,
filter: ProductSyncerOptions["filter"]
}
export class SyncService {
readonly productSyncer: ProductSyncer
private readonly productSyncer: ProductSyncer;
private queue: SyncQueue;
constructor(printful: PrintfulClient, webflow: WebflowClient) {
this.productSyncer = new ProductSyncer(printful, webflow, log);
this.queue = new SyncQueue();
}
async start(filter: SyncOptions["filter"]) {
let id: string | null = null;
async sync(opt: SyncServiceOptions) {
let id = opt.id ?? null;
try {
log.info("starting sync run");
await this.productSyncer.sync({
filter,
filter: opt.filter,
onStart: async (startDate) => {
const result = await db.insertInto("product_syncs")
.values({
started_at: startDate,
printful_product_id_filter: filter?.printfulProductIds,
syncing_printful_product_ids: []
})
.returning("id")
.executeTakeFirstOrThrow();
id = result.id;
const values = {
started_at: startDate,
printful_product_id_filter: opt.filter?.printfulProductIds ?? null,
};
if (id) {
await db.updateTable("product_syncs")
.set(values)
.where("id", "=", id)
.execute();
}
else {
const result = await db.insertInto("product_syncs")
.values(values)
.returning("id")
.executeTakeFirstOrThrow();
id = result.id;
}
},
onStep: async (printfulProductIds: number[]) => {
if (!id) throw new Error("Expected sync run ID");
@@ -35,14 +78,29 @@ export class SyncService {
onCompletion: async (completionDate, duration) => {
if (!id) throw new Error("Expected sync run ID");
await db.updateTable("product_syncs")
.set({ syncing_printful_product_ids: [], ended_at: completionDate, })
.set({ syncing_printful_product_ids: [], ended_at: completionDate })
.where("id", "=", id)
.execute();
// Sync next item in queue
const nextSync = await this.queue.front();
if (nextSync) {
await this.sync({
id: nextSync.id,
filter: { printfulProductIds: nextSync.printful_product_id_filter }
});
}
}
});
}
catch (err) {
if (id) {
const isActiveSyncConflict = err instanceof DatabaseError && err.code === "23505" && (await this.getActiveSync());
if (isActiveSyncConflict) {
log.info("sync already active, enqueuing next sync");
await this.queue.enqueue(opt);
return;
}
else if (id) {
await db.updateTable("product_syncs")
.set({ syncing_printful_product_ids: [], ended_at: new Date(), has_failed: true })
.where("id", "=", id)
@@ -52,7 +110,12 @@ export class SyncService {
}
}
async getActiveRun() {
return await db.selectFrom("product_syncs").selectAll().where("ended_at", "is", null).executeTakeFirst();
async getActiveSync() {
return await db.selectFrom("product_syncs")
.selectAll()
.where("started_at", "is not", null)
.where("ended_at", "is", null)
.limit(1)
.executeTakeFirst();
}
}