diff --git a/.gitignore b/.gitignore index 591ab2e..1c6e801 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ **/node_modules/** dist/** desktop.ini +.continue/** \ No newline at end of file diff --git a/src/global.ts b/src/global.ts index fd27715..1110434 100644 --- a/src/global.ts +++ b/src/global.ts @@ -14,6 +14,10 @@ import { createMissingPhotoFolders } from "./createMissingPhotoFolders" import { reinstallTriggers } from "./triggers" import { newSkuHandler } from "./newSku" import { columnOnEditHandler } from "./OnEditHandler" +import { + onEditQueue, + processBatchedEdits +} from "./onEditQueue" import { fillProductFromTemplate } from "./fillProductFromTemplate" // prettier-ignore @@ -25,6 +29,8 @@ import { fillProductFromTemplate } from "./fillProductFromTemplate" ;(global as any).matchProductToShopifyOnEditHandler = matchProductToShopifyOnEditHandler ;(global as any).updateShopifyProductHandler = updateShopifyProductHandler ;(global as any).columnOnEditHandler = columnOnEditHandler +;(global as any).onEditQueue = onEditQueue +;(global as any).processBatchedEdits = processBatchedEdits ;(global as any).reauthorizeScript = reauthorizeScript ;(global as any).reinstallTriggers = reinstallTriggers ;(global as any).newSkuHandler = newSkuHandler diff --git a/src/newSku.ts b/src/newSku.ts index 9f091f5..2171b36 100644 --- a/src/newSku.ts +++ b/src/newSku.ts @@ -5,6 +5,7 @@ import { getColumnValuesByName, } from "./sheetUtils" +const LOCK_TIMEOUT_MS = 1000 * 10 export function newSkuHandler(e: GoogleAppsScript.Events.SheetsOnEdit) { var sheet = SpreadsheetApp.getActive().getActiveSheet() @@ -20,7 +21,14 @@ export function newSkuHandler(e: GoogleAppsScript.Events.SheetsOnEdit) { console.log("new ID was not requested, returning") return } - newSku(row) + // Acquire a user lock to prevent multiple onEdit calls from clashing + const documentLock = LockService.getDocumentLock() + try { + documentLock.waitLock(LOCK_TIMEOUT_MS) + newSku(row) + } finally { + documentLock.releaseLock() + } } export function newSku(row: number) { diff --git a/src/onEditQueue.ts b/src/onEditQueue.ts new file mode 100644 index 0000000..f1f705b --- /dev/null +++ b/src/onEditQueue.ts @@ -0,0 +1,109 @@ +import { getCellValueByColumnName } from "./sheetUtils" +import { Product } from "./Product" +import { Shop } from "./shopifyApi" + +// --- Constants --- +const BATCH_INTERVAL_MS = 30 * 1000 // 30 seconds +const LOCK_TIMEOUT_MS = 10 * 1000 // 10 seconds for lock acquisition +const CACHE_KEY_EDITS = "pendingEdits" +const CACHE_KEY_LAST_EDIT_TIME = "lastEditTime" +const SCRIPT_PROPERTY_TRIGGER_SCHEDULED = "batchTriggerScheduled" + +export function onEditQueue(e) { + const sheet = e.source.getActiveSheet() + if (sheet.getName() !== "product_inventory") { + console.log("skipping edit on sheet " + sheet.getName()) + return + } + const range = e.range + const row = range.getRow() + const sku = getCellValueByColumnName(sheet, "sku", row) + if (!sku) { + console.log("No SKU found for row " + row) + return + } + // Acquire a user lock to prevent multiple onEdit calls from clashing + const documentLock = LockService.getDocumentLock() + try { + documentLock.waitLock(LOCK_TIMEOUT_MS) + + const scriptProperties = PropertiesService.getScriptProperties() // Shared cache for all users + + // 1. Accumulate edits in cache + let pendingEdits = [] + try { + pendingEdits = JSON.parse( + scriptProperties.getProperty(CACHE_KEY_EDITS) || "[]" + ) + } catch (e) { + console.log("Cache corruption: " + e.message) + scriptProperties.setProperty(CACHE_KEY_EDITS, "[]") + } + const existingIndex = pendingEdits.findIndex((item) => item.sku === sku) + if (existingIndex !== -1) { + console.log("New edit on queued SKU '"+sku+"', resetting timer...") + pendingEdits[existingIndex].timestamp = Date.now() + } else { + console.log("New SKU '"+sku+"' added to queue.") + pendingEdits.push({ sku, timestamp: Date.now() }) + } + scriptProperties.setProperty(CACHE_KEY_EDITS, JSON.stringify(pendingEdits)) + } catch (error) { + console.log( + "Error in onEdit (lock acquisition or cache operation): " + error.message + ) + } finally { + documentLock.releaseLock() + } +} + +export function processBatchedEdits() { + const scriptLock = LockService.getScriptLock() // Use script lock for the processing function + try { + scriptLock.waitLock(LOCK_TIMEOUT_MS) + + const scriptProperties = PropertiesService.getScriptProperties() + + let pendingEdits = [] + try { + const pendingEditsStr = scriptProperties.getProperty(CACHE_KEY_EDITS) + pendingEdits = pendingEditsStr ? JSON.parse(pendingEditsStr) : [] + } catch (e) { + console.log("Cache corruption: " + e.message) + scriptProperties.setProperty(CACHE_KEY_EDITS, "[]") + } + + console.log(`Total SKUs in queue: ${pendingEdits.length}`) + const now = Date.now() + const toProcess = pendingEdits.filter( + (edit) => now - edit.timestamp > BATCH_INTERVAL_MS + ) + if (toProcess.length > 0) { + let shop = new Shop() + console.log(`Processing ${toProcess.length} SKUs...`) + toProcess.forEach((edit) => { + console.log( + `Processing SKU ${edit.sku}, Timestamp: ${new Date(edit.timestamp)}` + ) + let p = new Product(edit.sku) + p.UpdateShopifyProduct(shop) + }) + + pendingEdits = pendingEdits.filter( + edit => !toProcess.some(p => p.sku === edit.sku) + ); + scriptProperties.setProperty(CACHE_KEY_EDITS, JSON.stringify(pendingEdits)); + + console.log(`Processed ${toProcess.length} edits.`) + } else { + console.log("No pending edits to process.") + } + } catch (error) { + console.log( + "Error in processBatchedEdits (lock acquisition or processing): " + + error.message + ) + } finally { + scriptLock.releaseLock() + } +} diff --git a/src/triggers.ts b/src/triggers.ts index 8fea39b..a6c78d9 100644 --- a/src/triggers.ts +++ b/src/triggers.ts @@ -10,5 +10,11 @@ export function reinstallTriggers() { .forSpreadsheet(ss) .onEdit() .create() - ScriptApp.newTrigger("columnOnEditHandler").forSpreadsheet(ss).onEdit().create() + // ScriptApp.newTrigger("columnOnEditHandler").forSpreadsheet(ss).onEdit().create() + // ScriptApp.newTrigger("onEditQueue").forSpreadsheet(ss).onEdit().create() + ScriptApp.newTrigger("onEditQueue").forSpreadsheet(ss).onEdit().create() + ScriptApp.newTrigger("processBatchedEdits") + .timeBased() + .everyMinutes(1) + .create() }