diff --git a/apps/api/package.json b/apps/api/package.json index d0f0258..9f8290f 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -11,9 +11,7 @@ "scripts": { "dev": "bun run --hot src/index.ts", "start": "bun run src/index.ts", - "build": "bun build --compile --minify-whitespace --minify-syntax --target bun-linux-x64 --outfile dist/server src/index.ts", - "db:codegen": "bunx kysely-codegen --env-file .env.development", - "db:seed": "bun run src/database/seed.ts" + "build": "bun build --compile --minify-whitespace --minify-syntax --target bun-linux-x64 --outfile dist/server src/index.ts" }, "dependencies": { "@blade-and-brawn/calculator": "workspace:*", @@ -25,13 +23,13 @@ "@types/pg": "^8.20.0", "elysia": "^1.4.29", "kysely": "^0.29.2", - "kysely-codegen": "^0.20.0", "ml-levenberg-marquardt": "^5.0.1", "pg": "^8.22.0", "pino": "^10.3.1", "zipcodes-us": "^1.1.3" }, "devDependencies": { - "pino-pretty": "^13.1.3" + "pino-pretty": "^13.1.3", + "kysely-codegen": "^0.20.0" } } diff --git a/apps/api/src/database/seed.ts b/apps/api/src/database/seed.ts index ffad2ee..3aab615 100644 --- a/apps/api/src/database/seed.ts +++ b/apps/api/src/database/seed.ts @@ -6,12 +6,26 @@ import { log } from "../util"; // SYNC RUNS // ===================== { - log.info({ name: "sync_runs" }, "Creating table"); - await db.schema.dropTable("sync_runs").ifExists().execute() - await db.schema.createTable("sync_runs") + log.info({ name: "product_syncs" }, "Creating table"); + await db.schema.dropTable("product_syncs").ifExists().execute() + await db.schema.createTable("product_syncs") .$call(addDefaultColumns) - .addColumn("completed_at", "timestamptz") + .addColumn("printful_product_id_filter", sql`integer[]`) + .addColumn("syncing_printful_product_ids", sql`integer[]`, (cb) => cb.notNull()) + .addColumn("started_at", "timestamptz", (cb) => cb.unique()) + .addColumn("ended_at", "timestamptz") + .addColumn("has_failed", "boolean", (cb) => cb.notNull().defaultTo(false)) .execute() + + await db.schema.dropIndex("idx_one_active_product_sync").ifExists().execute(); + await db.schema.createIndex("idx_one_active_product_sync") + .on("product_syncs") + .unique() + .column("ended_at") + .where(sql.ref("started_at"), "is not", null) + .where("ended_at", "is", null) + .nullsNotDistinct() + .execute(); } function addDefaultColumns(ctb: CreateTableBuilder) { @@ -23,6 +37,5 @@ function addDefaultColumns(ctb: CreateTableBuilder) { .addColumn("created_at", "timestamptz", (cb) => cb .notNull() .defaultTo(sql`now()`) - .unique() ) }; diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index 26a0c46..b909c01 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -13,7 +13,6 @@ import { Elysia, NotFoundError, status, t } from "elysia"; import { PrintfulClient, PrintfulError, - ProductSyncer, WebflowClient, WebflowError, Printful, @@ -23,6 +22,7 @@ import zipcodesUs from "zipcodes-us"; import { env, log } from "./util"; import serverTiming from "@elysia/server-timing"; import jwt from "@elysia/jwt"; +import { SyncService } from "./services/sync"; // SERVICES // ----------- @@ -42,7 +42,7 @@ const webflow = new WebflowClient({ webhookSecret: env.WEBFLOW_WEBHOOK_SECRET, }); -const productSyncer = new ProductSyncer(printful, webflow, log); +const syncService = new SyncService(printful, webflow); // ELYSIA // ----------- @@ -143,15 +143,22 @@ export const app = new Elysia() .group("/sync", (app) => app // Sync status - .get("/", async ({ }) => ({ - state: await productSyncer.getState(), - isRunning: await productSyncer.isRunning() - })) + .get("/", async ({ }) => { + const activeRun = await syncService.getActiveRun(); + if (!activeRun) throw new NotFoundError("No sync in progress"); + return { + startDate: activeRun.started_at, + syncingPrintfulProductIds: activeRun.syncing_printful_product_ids + } + }) // Run sync .post("/:printfulProductId?", async ({ params: { printfulProductId } }) => { log.debug({ printfulProductId }); - const canSync = await productSyncer.sync({ filter: { printfulProductIds: printfulProductId } }); - if (!canSync) throw status(409, "Sync already in progress"); + await syncService.start({ + printfulProductIds: printfulProductId ? + [printfulProductId] : + undefined + }); }, { params: t.Object({ printfulProductId: t.Optional(t.Numeric()) }) }), ) .get("/:printfulProductId", async ({ params }) => { @@ -178,7 +185,7 @@ export const app = new Elysia() case Printful.Webhook.Event.ProductUpdated: { const printfulProduct = payload.data.sync_product; log.info({ productId: printfulProduct.id }, "printful webhook: product updated"); - await productSyncer.sync({ filter: { printfulProductIds: printfulProduct.id } }); + await syncService.start({ printfulProductIds: [printfulProduct.id] }); break; } case Printful.Webhook.Event.ProductDeleted: { diff --git a/apps/api/src/services/sync.ts b/apps/api/src/services/sync.ts new file mode 100644 index 0000000..0a1237a --- /dev/null +++ b/apps/api/src/services/sync.ts @@ -0,0 +1,58 @@ +import { PrintfulClient, ProductSyncer, WebflowClient, type SyncOptions } from "@blade-and-brawn/commerce"; +import { log } from "../util"; +import { db } from "../database/db"; + +export class SyncService { + readonly productSyncer: ProductSyncer + + constructor(printful: PrintfulClient, webflow: WebflowClient) { + this.productSyncer = new ProductSyncer(printful, webflow, log); + } + + async start(filter: SyncOptions["filter"]) { + let id: string | null = null; + try { + await this.productSyncer.sync({ + 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; + }, + onStep: async (printfulProductIds: number[]) => { + if (!id) throw new Error("Expected sync run ID"); + await db.updateTable("product_syncs") + .set({ syncing_printful_product_ids: printfulProductIds }) + .where("id", "=", id) + .execute(); + }, + 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, }) + .where("id", "=", id) + .execute(); + } + }); + } + catch (err) { + if (id) { + await db.updateTable("product_syncs") + .set({ syncing_printful_product_ids: [], ended_at: new Date(), has_failed: true }) + .where("id", "=", id) + .execute(); + } + throw err; + } + } + + async getActiveRun() { + return await db.selectFrom("product_syncs").selectAll().where("ended_at", "is", null).executeTakeFirst(); + } +} diff --git a/apps/portal/package.json b/apps/portal/package.json index 4b2b595..8cf13fd 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -26,7 +26,7 @@ "@blade-and-brawn/domain": "workspace:*", "@elysia/eden": "^1.4.10", "@tailwindcss/vite": "^4.3.2", - "daisyui": "^5.6.6", + "daisyui": "^5.6.7", "jose": "^6.2.3", "tailwindcss": "^4.3.2" } diff --git a/apps/portal/src/routes/(app)/commerce/+page.svelte b/apps/portal/src/routes/(app)/commerce/+page.svelte index ac32768..8e6c83a 100644 --- a/apps/portal/src/routes/(app)/commerce/+page.svelte +++ b/apps/portal/src/routes/(app)/commerce/+page.svelte @@ -7,13 +7,16 @@ const synchronizer = $state({ isRunning: false, - printfulProductIds: [] as number[], + syncingPrintfulProductIds: [] as number[], poll: async function () { const res = await api.products.sync.get(); - if (res.data) { - synchronizer.isRunning = res.data.isRunning; - synchronizer.printfulProductIds = - res.data.state.printfulProductIds; + if (res.error) { + synchronizer.isRunning = false; + synchronizer.syncingPrintfulProductIds = []; + } else { + synchronizer.isRunning = true; + synchronizer.syncingPrintfulProductIds = + res.data.syncingPrintfulProductIds; } }, startPolling: () => { @@ -93,7 +96,7 @@ > {#await isProductSynced(printfulProduct) then isSynced} - {#if synchronizer.printfulProductIds.includes(printfulProduct.id)} + {#if synchronizer.syncingPrintfulProductIds.includes(printfulProduct.id)}
Syncing...
diff --git a/bun.lock b/bun.lock index 3c5bcb2..76b3774 100644 --- a/bun.lock +++ b/bun.lock @@ -21,13 +21,13 @@ "@types/pg": "^8.20.0", "elysia": "^1.4.29", "kysely": "^0.29.2", - "kysely-codegen": "^0.20.0", "ml-levenberg-marquardt": "^5.0.1", "pg": "^8.22.0", "pino": "^10.3.1", "zipcodes-us": "^1.1.3", }, "devDependencies": { + "kysely-codegen": "^0.20.0", "pino-pretty": "^13.1.3", }, }, @@ -358,7 +358,7 @@ "cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="], - "daisyui": ["daisyui@5.6.6", "", {}, "sha512-UFiuIZYucF7ylrnY3PT1SqwaEbVB10mqTZRyeRchZk0bud+HihYLLhXdxGDVmR/ftrM9G9YVr9FFPRVVIZJ5hA=="], + "daisyui": ["daisyui@5.6.7", "", {}, "sha512-FVpuUbesyAUBWNp5pXn9JDF1EWt3IZJzp5Staw1YqfP0MwL36sv3wtmfWeuEBkZcazaQbeZXA91oW5dSB+TUEg=="], "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], diff --git a/package.json b/package.json index e282053..0fbd688 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "dev:api": "bun run --filter @blade-and-brawn/api dev", "build:api": "bun run --filter @blade-and-brawn/api build", "dev:portal": "bun run --filter @blade-and-brawn/portal dev", - "build:portal": "bun run --filter @blade-and-brawn/portal build" + "build:portal": "bun run --filter @blade-and-brawn/portal build", + "db:codegen": "bunx kysely-codegen --env-file .env.development", + "db:seed": "bun run apps/api/src/database/seed.ts" }, "devDependencies": { "@types/bun": "^1.3.14", diff --git a/packages/commerce/src/sync.ts b/packages/commerce/src/sync.ts index 268f5f0..dbba7b6 100644 --- a/packages/commerce/src/sync.ts +++ b/packages/commerce/src/sync.ts @@ -2,7 +2,6 @@ import type { Printful, Webflow } from "./util/types"; import { formatSlug, type DeepPartial } from "./util/misc"; import { WebflowClient } from "./webflow"; import { PrintfulClient } from "./printful"; -import { redis } from "bun"; import pino from "pino"; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -18,14 +17,13 @@ type MetaPrintfulProduct = { entries: ProductEntry[]; }; -type SyncState = { - printfulProductIds: number[]; -}; - -type SyncOptions = { +export type SyncOptions = { filter?: { - printfulProductIds?: number[] | number; - } + printfulProductIds?: number[]; + }, + onStart?: (startDate: Date) => Promise + onCompletion?: (completionDate: Date, duration: number) => Promise + onStep?: (printfulProductIds: number[]) => Promise }; export class ProductSyncer { @@ -39,183 +37,170 @@ export class ProductSyncer { this.log = (log ?? pino({ level: "silent" })).child({ component: "ProductSyncer" }); } - async sync(opt: SyncOptions = {}): Promise { - if (typeof opt.filter?.printfulProductIds === "number") - opt.filter.printfulProductIds = [opt.filter.printfulProductIds]; + async sync(opt: SyncOptions = {}) { + this.log.info("starting sync run"); + const startDate = new Date(); + await opt.onStart?.(startDate); - this.log.info("attempting sync run"); - const canRun = await this.run(); - if (!canRun) return false; + if (opt.filter?.printfulProductIds) + this.log.info({ printfulProductIds: opt.filter.printfulProductIds }, "sync started (filtered)"); + else + this.log.info("sync started (all)"); - try { - const syncStart = Date.now(); - if (opt.filter?.printfulProductIds) - this.log.info({ printfulProductIds: opt.filter.printfulProductIds }, "sync started (filtered)"); - else - this.log.info("sync started (all)"); + this.log.debug("retrieving webflow products"); + const allWebflowProducts = await this.webflow.Products.list({ forceAll: true }); - this.log.debug("retrieving webflow products"); - const allWebflowProducts = await this.webflow.Products.list({ forceAll: true }); + this.log.debug("retrieving printful products"); + const allPrintfulProducts = await this.printful.Products.list({ forceAll: true }); - this.log.debug("retrieving printful products"); - const allPrintfulProducts = await this.printful.Products.list({ forceAll: true }); + this.log.info({ printfulProductCount: allPrintfulProducts.length, filter: opt.filter ?? "N/A" }, "generating meta printful products"); + const metaPrintfulProducts = await this.generateMetaPrintfulProducts(allWebflowProducts, allPrintfulProducts, opt); - this.log.info({ printfulProductCount: allPrintfulProducts.length, filter: opt.filter ?? "N/A" }, "generating meta printful products"); - const metaPrintfulProducts = await this.generateMetaPrintfulProducts(allWebflowProducts, allPrintfulProducts, opt); + for (const metaPrintfulProduct of metaPrintfulProducts) { + const printfulProductIds = metaPrintfulProduct.entries.map((e) => e.product.sync_product.id); + this.log.info({ name: metaPrintfulProduct.name, externalId: metaPrintfulProduct.webflowProductId, printfulProductIds }, "syncing meta printful product"); + await opt.onStep?.(printfulProductIds); - for (const metaPrintfulProduct of metaPrintfulProducts) { - this.log.info({ name: metaPrintfulProduct.name, externalId: metaPrintfulProduct.webflowProductId }, "syncing meta printful product"); + const newWebflowSkus: DeepPartial[] = this.generateWebflowSkus(metaPrintfulProduct); + const foundColors = new Set( + newWebflowSkus.map((sku) => sku.fieldData?.["sku-values"]?.["color"]).filter((v) => v !== undefined) + ); + const foundSizes = new Set( + newWebflowSkus.map((sku) => sku.fieldData?.["sku-values"]?.["size"]).filter((v) => v !== undefined) + ); - await this.setState({ - printfulProductIds: metaPrintfulProduct.entries.map((e) => e.product.sync_product.id), + if (!newWebflowSkus[0]) throw new Error("no webflow SKUs generated — all variants were filtered out"); + + const webflowProductFieldData = { + name: metaPrintfulProduct.name, + slug: formatSlug(metaPrintfulProduct.name), + shippable: true, + "tax-category": "standard-taxable", + "sku-properties": [ + { + id: "color", + name: "Color", + enum: Array.from(foundColors).map((color) => ({ + id: color, + slug: formatSlug(color), + name: color, + })), + }, + { + id: "size", + name: "Size", + enum: Array.from(foundSizes).map((size) => ({ + id: size, + slug: formatSlug(size), + name: size, + })), + }, + ], + }; + + const webflowProduct = allWebflowProducts.find((wp) => wp.product.id === metaPrintfulProduct.webflowProductId); + + if (webflowProduct) { + this.log.info({ id: webflowProduct.product.id }, "existing webflow product found, updating it"); + + // do not update images + for (const sku of newWebflowSkus) delete sku.fieldData?.["main-image"]; + + // handle case where webflow SKU created by associated printful variant external_id update failed + if (!newWebflowSkus[0].id) { + this.log.warn({ webflowSku: newWebflowSkus[0] }, "missing webflow SKU id"); + const webflowSkuMatch = this.resolveWebflowSku( + webflowProduct.skus, + newWebflowSkus[0].fieldData?.["sku-values"]?.["color"], + newWebflowSkus[0].fieldData?.["sku-values"]?.["size"], + ); + + if (webflowSkuMatch) + newWebflowSkus[0].id = webflowSkuMatch.id; + else + this.log.warn({ webflowSku: newWebflowSkus[0] }, "could not find existing webflow SKU that matches"); + } + + await this.webflow.Products.update(webflowProduct.product.id, { + product: { fieldData: webflowProductFieldData }, + sku: newWebflowSkus[0], }); - const newWebflowSkus: DeepPartial[] = this.generateWebflowSkus(metaPrintfulProduct); - const foundColors = new Set( - newWebflowSkus.map((sku) => sku.fieldData?.["sku-values"]?.["color"]).filter((v) => v !== undefined) - ); - const foundSizes = new Set( - newWebflowSkus.map((sku) => sku.fieldData?.["sku-values"]?.["size"]).filter((v) => v !== undefined) - ); - - if (!newWebflowSkus[0]) throw new Error("no webflow SKUs generated — all variants were filtered out"); - - const webflowProductFieldData = { - name: metaPrintfulProduct.name, - slug: formatSlug(metaPrintfulProduct.name), - shippable: true, - "tax-category": "standard-taxable", - "sku-properties": [ - { - id: "color", - name: "Color", - enum: Array.from(foundColors).map((color) => ({ - id: color, - slug: formatSlug(color), - name: color, - })), - }, - { - id: "size", - name: "Size", - enum: Array.from(foundSizes).map((size) => ({ - id: size, - slug: formatSlug(size), - name: size, - })), - }, - ], - }; - - const webflowProduct = allWebflowProducts.find((wp) => wp.product.id === metaPrintfulProduct.webflowProductId); - - if (webflowProduct) { - this.log.info({ id: webflowProduct.product.id }, "existing webflow product found, updating it"); - - // do not update images - for (const sku of newWebflowSkus) delete sku.fieldData?.["main-image"]; - - // handle case where webflow SKU created by associated printful variant external_id update failed - if (!newWebflowSkus[0].id) { - this.log.warn({ webflowSku: newWebflowSkus[0] }, "missing webflow SKU id"); - const webflowSkuMatch = this.resolveWebflowSku( - webflowProduct.skus, - newWebflowSkus[0].fieldData?.["sku-values"]?.["color"], - newWebflowSkus[0].fieldData?.["sku-values"]?.["size"], + for (const newWebflowSku of newWebflowSkus) { + const existingWebflowSku = this.resolveWebflowSku( + webflowProduct.skus, + newWebflowSku.fieldData?.["sku-values"]?.["color"], + newWebflowSku.fieldData?.["sku-values"]?.["size"], + newWebflowSku.id, + ); + if (existingWebflowSku) { + await this.webflow.Products.Skus.update( + metaPrintfulProduct.webflowProductId, + existingWebflowSku.id, + newWebflowSku, ); - - if (webflowSkuMatch) - newWebflowSkus[0].id = webflowSkuMatch.id; - else - this.log.warn({ webflowSku: newWebflowSkus[0] }, "could not find existing webflow SKU that matches"); - } - - await this.webflow.Products.update(webflowProduct.product.id, { - product: { fieldData: webflowProductFieldData }, - sku: newWebflowSkus[0], - }); - - for (const newWebflowSku of newWebflowSkus) { - const existingWebflowSku = this.resolveWebflowSku( - webflowProduct.skus, - newWebflowSku.fieldData?.["sku-values"]?.["color"], - newWebflowSku.fieldData?.["sku-values"]?.["size"], - newWebflowSku.id, + } else { + await this.webflow.Products.Skus.create( + metaPrintfulProduct.webflowProductId, + [newWebflowSku], ); - if (existingWebflowSku) { - await this.webflow.Products.Skus.update( - metaPrintfulProduct.webflowProductId, - existingWebflowSku.id, - newWebflowSku, - ); - } else { - await this.webflow.Products.Skus.create( - metaPrintfulProduct.webflowProductId, - [newWebflowSku], - ); - } } - } else { - this.log.info("existing webflow product not found, creating it"); - - const webflowProductId = await this.webflow.Products.create({ - product: { fieldData: webflowProductFieldData }, - sku: newWebflowSkus[0], - }); - metaPrintfulProduct.webflowProductId = webflowProductId; - - if (newWebflowSkus.length > 1) - await this.webflow.Products.Skus.create(webflowProductId, newWebflowSkus.slice(1)); } + } else { + this.log.info("existing webflow product not found, creating it"); - // Re-fetch after SKU mutations so newly created SKUs have their real IDs. - const freshWebflowProduct = await this.webflow.Products.get(metaPrintfulProduct.webflowProductId); - if (!freshWebflowProduct) throw new Error("webflow product missing"); + const webflowProductId = await this.webflow.Products.create({ + product: { fieldData: webflowProductFieldData }, + sku: newWebflowSkus[0], + }); + metaPrintfulProduct.webflowProductId = webflowProductId; - // Update printful external IDs - for (const { colorGroup, product: printfulProduct } of metaPrintfulProduct.entries) { - const expectedExternalId = colorGroup ? - `${metaPrintfulProduct.webflowProductId}-${colorGroup}` : - metaPrintfulProduct.webflowProductId; - if (printfulProduct.sync_product.external_id === expectedExternalId) continue; - - const printfulVariantPatches: DeepPartial[] = []; - for (const printfulVariant of printfulProduct.sync_variants) { - const webflowSku = this.resolveWebflowSku( - freshWebflowProduct.skus, - colorGroup ?? printfulVariant.color, - printfulVariant.size, - ); - if (webflowSku) { - printfulVariantPatches.push({ - id: printfulVariant.id, - external_id: String(webflowSku.id), - }); - } - } - - await sleep(10000); - this.log.info({ printfulProduct: printfulProduct.sync_product.id, externalId: expectedExternalId }, "updating printful product's external ID"); - await this.printful.Products.update(printfulProduct.sync_product.id, { - sync_product: { - id: printfulProduct.sync_product.id, - external_id: expectedExternalId, - }, - sync_variants: printfulVariantPatches, - }); - } + if (newWebflowSkus.length > 1) + await this.webflow.Products.Skus.create(webflowProductId, newWebflowSkus.slice(1)); } - this.log.info({ durationMs: Date.now() - syncStart }, "sync complete"); - } finally { - try { - await redis.del("commerce:sync:lock"); - await redis.del("commerce:sync:state"); - } catch { - this.log.error("failed to reset sync state"); + // Re-fetch after SKU mutations so newly created SKUs have their real IDs. + const freshWebflowProduct = await this.webflow.Products.get(metaPrintfulProduct.webflowProductId); + if (!freshWebflowProduct) throw new Error("webflow product missing"); + + // Update printful external IDs + for (const { colorGroup, product: printfulProduct } of metaPrintfulProduct.entries) { + const expectedExternalId = colorGroup ? + `${metaPrintfulProduct.webflowProductId}-${colorGroup}` : + metaPrintfulProduct.webflowProductId; + if (printfulProduct.sync_product.external_id === expectedExternalId) continue; + + const printfulVariantPatches: DeepPartial[] = []; + for (const printfulVariant of printfulProduct.sync_variants) { + const webflowSku = this.resolveWebflowSku( + freshWebflowProduct.skus, + colorGroup ?? printfulVariant.color, + printfulVariant.size, + ); + if (webflowSku) { + printfulVariantPatches.push({ + id: printfulVariant.id, + external_id: String(webflowSku.id), + }); + } + } + + await sleep(10000); + this.log.info({ printfulProduct: printfulProduct.sync_product.id, externalId: expectedExternalId }, "updating printful product's external ID"); + await this.printful.Products.update(printfulProduct.sync_product.id, { + sync_product: { + id: printfulProduct.sync_product.id, + external_id: expectedExternalId, + }, + sync_variants: printfulVariantPatches, + }); } } - return true; + + const endDate = new Date(); + const duration = endDate.getTime() - startDate.getTime(); + await opt.onCompletion?.(endDate, duration); + this.log.info({ durationMs: duration }, "sync complete"); } parseColorGroup(name: string): string | null { @@ -273,9 +258,6 @@ export class ProductSyncer { allPrintfulProducts: Printful.Products.SyncProduct[], opt: SyncOptions, ) { - if (typeof opt.filter?.printfulProductIds === "number") - opt.filter.printfulProductIds = [opt.filter.printfulProductIds]; - const metaNameFilter = opt.filter?.printfulProductIds ?.map((id) => allPrintfulProducts.find((p) => p.id === id)) ?.filter((p) => p !== undefined) @@ -341,22 +323,4 @@ export class ProductSyncer { private isPrintfulProductSynced(printfulProduct: Printful.Products.SyncProduct, allWebflowProducts: Webflow.Products.ProductAndSkus[]) { return allWebflowProducts.some((wp) => wp.product.id === printfulProduct.external_id.split("-")[0]); } - - async isRunning(): Promise { - return !!await redis.get("commerce:sync:lock"); - } - - async getState(): Promise { - const val = await redis.get("commerce:sync:state"); - if (val) return JSON.parse(val); - return { printfulProductIds: [] }; - } - - private async setState(state: SyncState) { - await redis.set("commerce:sync:state", JSON.stringify(state), "EX", 1800); - } - - private async run(): Promise { - return !!await redis.set("commerce:sync:lock", "1", "NX", "EX", "1800"); - } }