[object Object]

← back to Designer Wallcoverings

TK-135: harden apply scripts — gqlR retries 502/gateway/network, null-safe loop, per-item try/catch, no crash on transient errors

512f21b7353cd73a3a80512f9a256bcdea8ff478 · 2026-07-27 22:07:48 -0700 · Steve

Files touched

Diff

commit 512f21b7353cd73a3a80512f9a256bcdea8ff478
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jul 27 22:07:48 2026 -0700

    TK-135: harden apply scripts — gqlR retries 502/gateway/network, null-safe loop, per-item try/catch, no crash on transient errors
---
 shopify/scripts/tk135-apply-descriptor.js | 32 ++++++++++++++++++++-----------
 shopify/scripts/tk135-apply-normalize.js  | 26 ++++++++++++++++---------
 2 files changed, 38 insertions(+), 20 deletions(-)

diff --git a/shopify/scripts/tk135-apply-descriptor.js b/shopify/scripts/tk135-apply-descriptor.js
index 72770b5a..1997f39e 100644
--- a/shopify/scripts/tk135-apply-descriptor.js
+++ b/shopify/scripts/tk135-apply-descriptor.js
@@ -29,6 +29,7 @@ const TOKEN = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/
 const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
 const NOW = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
 const sleep = ms => new Promise(r => setTimeout(r, ms));
+process.on('unhandledRejection', e => console.error('⚠ unhandledRejection (continuing):', String(e).slice(0, 120)));
 
 function gql(query, variables = {}) {
   return new Promise((resolve, reject) => {
@@ -39,14 +40,19 @@ function gql(query, variables = {}) {
     req.on('error', reject); req.write(body); req.end();
   });
 }
-async function gqlR(q, v, tries = 4) {
+async function gqlR(q, v, tries = 8) {
   for (let i = 0; i < tries; i++) {
-    const r = await gql(q, v);
-    if (r && !r.errors) return r;
-    if (!(r.errors || []).some(e => /throttl/i.test(JSON.stringify(e)))) return r;
-    await sleep(1500 * (i + 1));
+    try {
+      const r = await gql(q, v);
+      if (r && !r.errors) return r;
+      const throttled = (r.errors || []).some(e => /throttl/i.test(JSON.stringify(e)));
+      if (!throttled) return r;
+    } catch (e) {
+      // 502/503/gateway/network/non-JSON body — transient, retry with backoff
+    }
+    await sleep(2000 * (i + 1));
   }
-  return gql(q, v);
+  return null;   // gave up gracefully; caller treats as a skippable failure, never crashes
 }
 const M_MF = `mutation($mf:[MetafieldsSetInput!]!){metafieldsSet(metafields:$mf){userErrors{field message}}}`;
 const M_ADD = `mutation($id:ID!,$tags:[String!]!){tagsAdd(id:$id,tags:$tags){userErrors{field message}}}`;
@@ -79,11 +85,15 @@ function load() {
   }
 
   for (const r of rows) {
-    let errs = [];
-    const a = await gqlR(M_ADD, { id: r.gid, tags: [r.val] }); errs = errs.concat(a.data?.tagsAdd?.userErrors || []);
-    const mf = await gqlR(M_MF, { mf: [{ ownerId: r.gid, namespace: 'custom', key: KEY, type: 'date_time', value: NOW }] }); errs = errs.concat(mf.data?.metafieldsSet?.userErrors || []);
-    if (errs.length) { report.failed.push({ gid: r.gid, errs }); console.log(`  ❌ ${r.gid.split('/').pop()} ${JSON.stringify(errs)}`); }
-    else { report.updated.push(r.gid); fs.appendFileSync(PROGRESS, r.gid + '\n'); }
+    try {
+      let errs = [];
+      const a = await gqlR(M_ADD, { id: r.gid, tags: [r.val] });
+      if (!a) errs.push({ message: 'tagsAdd: no response after retries' }); else errs = errs.concat(a.data?.tagsAdd?.userErrors || []);
+      const mf = await gqlR(M_MF, { mf: [{ ownerId: r.gid, namespace: 'custom', key: KEY, type: 'date_time', value: NOW }] });
+      if (!mf) errs.push({ message: 'metafield: no response after retries' }); else errs = errs.concat(mf.data?.metafieldsSet?.userErrors || []);
+      if (errs.length) { report.failed.push({ gid: r.gid, errs }); console.log(`  ❌ ${r.gid.split('/').pop()} ${JSON.stringify(errs)}`); }
+      else { report.updated.push(r.gid); fs.appendFileSync(PROGRESS, r.gid + '\n'); }
+    } catch (e) { report.failed.push({ gid: r.gid, errs: [String(e.message || e).slice(0, 140)] }); console.log(`  ⚠ ${r.gid.split('/').pop()} ${String(e.message || e).slice(0, 80)}`); }
     if (report.updated.length % 500 === 0 && report.updated.length) console.log(`  …${report.updated.length} tagged`);
     await sleep(150);
   }
diff --git a/shopify/scripts/tk135-apply-normalize.js b/shopify/scripts/tk135-apply-normalize.js
index 2bb09527..3db6be2c 100644
--- a/shopify/scripts/tk135-apply-normalize.js
+++ b/shopify/scripts/tk135-apply-normalize.js
@@ -27,6 +27,7 @@ const TOKEN = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/
 const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
 const NOW = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');   // per-SKU action timestamp (durable dedup + audit)
 const sleep = ms => new Promise(r => setTimeout(r, ms));
+process.on('unhandledRejection', e => console.error('⚠ unhandledRejection (continuing):', String(e).slice(0, 120)));
 
 function gql(query, variables = {}) {
   return new Promise((resolve, reject) => {
@@ -37,14 +38,19 @@ function gql(query, variables = {}) {
     req.on('error', reject); req.write(body); req.end();
   });
 }
-async function gqlR(q, v, tries = 4) {
+async function gqlR(q, v, tries = 8) {
   for (let i = 0; i < tries; i++) {
-    const r = await gql(q, v);
-    if (r && !r.errors) return r;
-    if (!(r.errors || []).some(e => /throttl/i.test(JSON.stringify(e)))) return r;
-    await sleep(1500 * (i + 1));
+    try {
+      const r = await gql(q, v);
+      if (r && !r.errors) return r;
+      const throttled = (r.errors || []).some(e => /throttl/i.test(JSON.stringify(e)));
+      if (!throttled) return r;
+    } catch (e) {
+      // 502/503/gateway/network/non-JSON body — transient, retry with backoff
+    }
+    await sleep(2000 * (i + 1));
   }
-  return gql(q, v);
+  return null;   // gave up gracefully; caller treats as skippable, never crashes
 }
 const M_MF = `mutation($mf:[MetafieldsSetInput!]!){metafieldsSet(metafields:$mf){userErrors{field message}}}`;
 const M_ADD = `mutation($id:ID!,$tags:[String!]!){tagsAdd(id:$id,tags:$tags){userErrors{field message}}}`;
@@ -80,15 +86,17 @@ function load() {
   }
 
   for (const r of rows) {
+   try {
     let errs = [];
     // per-SKU action timestamp (+ palette when present) — durable dedup/audit so re-runs never duplicate
     const mfs = [{ ownerId: r.gid, namespace: 'custom', key: 'color_normalized_at', type: 'date_time', value: NOW }];
     if (r.palette) mfs.push({ ownerId: r.gid, namespace: 'custom', key: 'color_palette', type: 'json', value: r.palette });
-    const mf = await gqlR(M_MF, { mf: mfs }); errs = errs.concat(mf.data?.metafieldsSet?.userErrors || []);
-    if (r.remove.length) { const d = await gqlR(M_DEL, { id: r.gid, tags: r.remove }); errs = errs.concat(d.data?.tagsRemove?.userErrors || []); }
-    if (r.add.length) { const a = await gqlR(M_ADD, { id: r.gid, tags: r.add }); errs = errs.concat(a.data?.tagsAdd?.userErrors || []); }
+    const mf = await gqlR(M_MF, { mf: mfs }); if (!mf) errs.push({ message: 'metafield: no response' }); else errs = errs.concat(mf.data?.metafieldsSet?.userErrors || []);
+    if (r.remove.length) { const d = await gqlR(M_DEL, { id: r.gid, tags: r.remove }); if (!d) errs.push({ message: 'tagsRemove: no response' }); else errs = errs.concat(d.data?.tagsRemove?.userErrors || []); }
+    if (r.add.length) { const a = await gqlR(M_ADD, { id: r.gid, tags: r.add }); if (!a) errs.push({ message: 'tagsAdd: no response' }); else errs = errs.concat(a.data?.tagsAdd?.userErrors || []); }
     if (errs.length) { report.failed.push({ gid: r.gid, errs }); console.log(`  ❌ ${r.gid.split('/').pop()} ${JSON.stringify(errs)}`); }
     else { report.updated.push(r.gid); fs.appendFileSync(PROGRESS, r.gid + '\n'); }
+   } catch (e) { report.failed.push({ gid: r.gid, errs: [String(e.message || e).slice(0, 140)] }); console.log(`  ⚠ ${r.gid.split('/').pop()} ${String(e.message || e).slice(0, 80)}`); }
     if (report.updated.length % 500 === 0 && report.updated.length) console.log(`  …${report.updated.length} normalized`);
     await sleep(150);
   }

← 02ef098b auto-save: 2026-07-27T21:54:54 (2 files) — shopify/scripts/d  ·  back to Designer Wallcoverings  ·  auto-save: 2026-07-27T22:25:06 (1 files) — shopify/scripts/d 80e46810 →