← 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
M shopify/scripts/tk135-apply-descriptor.jsM shopify/scripts/tk135-apply-normalize.js
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 →