Files
worldmonitor/scripts/seed-supply-chain-trade.mjs
Elie Habib 467608c2d7 chore: clear baseline lint debt (173 warnings → 49) (#1712)
Mechanical fixes across 13 files:
- isNaN() → Number.isNaN() (all values already numeric from parseFloat/parseInt)
- let → const where never reassigned
- Math.pow() → ** operator
- Unnecessary continue in for loop
- Useless string escape in test description
- Missing parseInt radix parameter
- Remove unused private class member (write-only counter)
- Prefix unused function parameter with _

Config: suppress noImportantStyles (CSS !important is intentional) and
useLiteralKeys (bracket notation used for computed/dynamic keys) in
biome.json. Remaining 49 warnings are all noExcessiveCognitiveComplexity
(already configured as warn, safe to address incrementally).
2026-03-16 08:48:00 +04:00

574 lines
26 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { loadEnvFile, CHROME_UA, runSeed, writeExtraKeyWithMeta, sleep, verifySeedKey } from './_seed-utils.mjs';
loadEnvFile(import.meta.url);
// ─── Keys (must match handler cache keys exactly) ───
const KEYS = {
shipping: 'supply_chain:shipping:v2',
barriers: 'trade:barriers:v1:tariff-gap:50',
restrictions: 'trade:restrictions:v1:tariff-overview:50',
customsRevenue: 'trade:customs-revenue:v1',
};
const SHIPPING_TTL = 3600;
const TRADE_TTL = 21600;
const MAJOR_REPORTERS = ['840', '156', '276', '392', '826', '356', '076', '643', '410', '036', '124', '484', '250', '380', '528'];
const WTO_MEMBER_CODES = {
'840': 'United States', '156': 'China', '276': 'Germany', '392': 'Japan',
'826': 'United Kingdom', '250': 'France', '356': 'India', '643': 'Russia',
'076': 'Brazil', '410': 'South Korea', '036': 'Australia', '124': 'Canada',
'484': 'Mexico', '380': 'Italy', '528': 'Netherlands', '000': 'World',
};
// ─── Shipping Rates (FRED) ───
const SHIPPING_SERIES = [
{ seriesId: 'PCU483111483111', name: 'Deep Sea Freight Producer Price Index', unit: 'index', frequency: 'm' },
{ seriesId: 'TSIFRGHT', name: 'Freight Transportation Services Index', unit: 'index', frequency: 'm' },
];
function detectSpike(history) {
if (!history || history.length < 3) return false;
const values = history.map(h => typeof h === 'number' ? h : h.value).filter(v => Number.isFinite(v));
if (values.length < 3) return false;
const mean = values.reduce((a, b) => a + b, 0) / values.length;
const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length;
const stdDev = Math.sqrt(variance);
if (stdDev === 0) return false;
return values[values.length - 1] > mean + 2 * stdDev;
}
async function fetchShippingRates() {
const apiKey = process.env.FRED_API_KEY;
if (!apiKey) throw new Error('Missing FRED_API_KEY');
const indices = [];
for (const cfg of SHIPPING_SERIES) {
try {
const params = new URLSearchParams({
series_id: cfg.seriesId, api_key: apiKey, file_type: 'json',
frequency: cfg.frequency, sort_order: 'desc', limit: '24',
});
const resp = await fetch(`https://api.stlouisfed.org/fred/series/observations?${params}`, {
headers: { Accept: 'application/json', 'User-Agent': CHROME_UA },
signal: AbortSignal.timeout(10_000),
});
if (!resp.ok) { console.warn(` FRED ${cfg.seriesId}: HTTP ${resp.status}`); continue; }
const data = await resp.json();
const observations = (data.observations || [])
.map(o => { const v = parseFloat(o.value); return Number.isNaN(v) || o.value === '.' ? null : { date: o.date, value: v }; })
.filter(Boolean).reverse();
if (observations.length === 0) continue;
const current = observations[observations.length - 1].value;
const previous = observations.length > 1 ? observations[observations.length - 2].value : current;
const changePct = previous !== 0 ? ((current - previous) / previous) * 100 : 0;
indices.push({
indexId: cfg.seriesId, name: cfg.name, currentValue: current, previousValue: previous,
changePct, unit: cfg.unit, history: observations, spikeAlert: detectSpike(observations),
});
await sleep(200);
} catch (e) {
console.warn(` FRED ${cfg.seriesId}: ${e.message}`);
}
}
console.log(` Shipping rates: ${indices.length} indices`);
return { indices, fetchedAt: new Date().toISOString(), upstreamUnavailable: false };
}
// ─── Container Indices (Shanghai Shipping Exchange) ───
async function fetchSSEIndex(indexName, indexId, dataItemType, displayName, unit) {
try {
const resp = await fetch(`https://en.sse.net.cn/currentIndex?indexName=${indexName}`, {
headers: { 'User-Agent': CHROME_UA, Accept: 'application/json' },
signal: AbortSignal.timeout(10_000),
});
if (!resp.ok) { console.warn(` SSE ${indexName}: HTTP ${resp.status}`); return []; }
const json = await resp.json();
const lines = json?.data?.lineDataList;
if (!Array.isArray(lines)) { console.warn(` SSE ${indexName}: no lineDataList`); return []; }
const composite = lines.find(l => l.dataItemTypeName === dataItemType);
if (!composite) { console.warn(` SSE ${indexName}: ${dataItemType} not found`); return []; }
const currentValue = composite.currentContent;
const previousValue = composite.lastContent;
if (typeof currentValue !== 'number') return [];
const changePct = typeof composite.percentage === 'number' ? composite.percentage
: (previousValue > 0 ? ((currentValue - previousValue) / previousValue) * 100 : 0);
const observationDate = json.data?.currentDate || new Date().toISOString().slice(0, 10);
return [{
indexId, name: displayName, currentValue, previousValue: previousValue ?? currentValue,
changePct, unit, history: [], spikeAlert: false, _observationDate: observationDate,
}];
} catch (e) {
console.warn(` SSE ${indexName}: ${e.message}`);
return [];
}
}
async function fetchSCFI() {
return fetchSSEIndex('scfi', 'SCFI', 'SCFI_T', 'SCFI - Shanghai Container Freight', 'index');
}
async function fetchCCFI() {
return fetchSSEIndex('ccfi', 'CCFI', 'CCFI_T', 'CCFI - China Container Freight', 'index');
}
// ─── Baltic Dry Index (HandyBulk scrape) ───
const BDI_INDEX_MAP = [
{ label: 'Dry', id: 'BDI', name: 'BDI - Baltic Dry Index' },
{ label: 'Capesize', id: 'BCI', name: 'BCI - Baltic Capesize Index' },
{ label: 'Panamax', id: 'BPI', name: 'BPI - Baltic Panamax Index' },
{ label: 'Supramax', id: 'BSI', name: 'BSI - Baltic Supramax Index' },
{ label: 'Handysize', id: 'BHSI', name: 'BHSI - Baltic Handysize Index' },
];
async function fetchBDI() {
try {
const resp = await fetch('https://www.handybulk.com/baltic-dry-index/', {
headers: { 'User-Agent': CHROME_UA, 'Accept-Encoding': 'gzip, deflate' },
signal: AbortSignal.timeout(10_000),
redirect: 'manual',
});
if (!resp.ok && resp.status !== 301 && resp.status !== 302) {
console.warn(` BDI: HTTP ${resp.status}`);
return [];
}
const contentLength = parseInt(resp.headers.get('content-length') || '0', 10);
if (contentLength > 1_000_000) { console.warn(' BDI: response too large'); return []; }
const html = await resp.text();
if (html.length > 1_000_000) { console.warn(' BDI: body too large'); return []; }
// Parse article date from heading (e.g., "13-March-2026" or "13-Mar-2026")
const dateMatch = html.match(/(\d{1,2})-(\w+)-(\d{4})/);
let articleDate = new Date().toISOString().slice(0, 10);
if (dateMatch) {
const parsed = new Date(`${dateMatch[2]} ${dateMatch[1]}, ${dateMatch[3]}`);
if (!Number.isNaN(parsed.getTime())) articleDate = parsed.toISOString().slice(0, 10);
}
const indices = [];
for (const cfg of BDI_INDEX_MAP) {
const patterns = [
new RegExp(`Baltic ${cfg.label} Index \\(${cfg.id}\\)[^.]*?(?:reach|to|at)\\s+([\\d,]+)\\s*points`, 'i'),
new RegExp(`${cfg.id}[^.]*?(?:reach|to|at)\\s+([\\d,]+)\\s*points`, 'i'),
new RegExp(`Baltic ${cfg.label} Index \\(${cfg.id}\\)[^.]*?([\\d,]+)\\s*points`, 'i'),
];
let currentValue = null;
for (const re of patterns) {
const m = html.match(re);
if (m) { currentValue = parseFloat(m[1].replace(/,/g, '')); break; }
}
if (currentValue == null || !Number.isFinite(currentValue)) continue;
let changePct = 0;
let previousValue = currentValue;
const deltaRe = new RegExp(`${cfg.id}\\)?[^.]*?(increased|decreased|gained|lost|dropped|rose)\\s+by\\s+([\\d,]+)\\s+points`, 'i');
const deltaMatch = html.match(deltaRe);
if (deltaMatch) {
const delta = parseFloat(deltaMatch[2].replace(/,/g, ''));
const isNeg = /decreased|lost|dropped/i.test(deltaMatch[1]);
const signedDelta = isNeg ? -delta : delta;
previousValue = currentValue - signedDelta;
changePct = previousValue !== 0 ? (signedDelta / previousValue) * 100 : 0;
}
indices.push({
indexId: cfg.id, name: cfg.name, currentValue, previousValue,
changePct, unit: 'index', history: [], spikeAlert: false, _observationDate: articleDate,
});
}
console.log(` BDI: ${indices.length} indices parsed`);
return indices;
} catch (e) {
console.warn(` BDI: ${e.message}`);
return [];
}
}
// ─── History accumulation (inline in canonical payload) ───
function accumulateHistory(newIndices, previousPayload) {
if (!previousPayload?.indices?.length) {
for (const idx of newIndices) delete idx._observationDate;
return newIndices;
}
const prevMap = new Map();
for (const idx of previousPayload.indices) {
if (idx.indexId) prevMap.set(idx.indexId, idx);
}
const fallbackDate = new Date().toISOString().slice(0, 10);
for (const idx of newIndices) {
const prev = prevMap.get(idx.indexId);
const existingHistory = prev?.history ?? [];
if (idx.history?.length > 0) { delete idx._observationDate; continue; }
const obsDate = idx._observationDate || fallbackDate;
const last = existingHistory[existingHistory.length - 1];
const newHistory = [...existingHistory];
if (!last || last.date !== obsDate) {
newHistory.push({ date: obsDate, value: idx.currentValue });
}
idx.history = newHistory.slice(-24);
delete idx._observationDate;
}
return newIndices;
}
// ─── WTO helpers ───
async function wtoFetch(path, params) {
const apiKey = process.env.WTO_API_KEY;
if (!apiKey) { console.warn('[WTO] WTO_API_KEY not set'); return null; }
const url = new URL(`https://api.wto.org/timeseries/v1${path}`);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const resp = await fetch(url.toString(), {
headers: { 'Ocp-Apim-Subscription-Key': apiKey, 'User-Agent': CHROME_UA },
signal: AbortSignal.timeout(15_000),
});
if (resp.status === 204) return { Dataset: [] };
if (!resp.ok) { console.warn(`[WTO] HTTP ${resp.status} for ${path}`); return null; }
return resp.json();
}
// ─── Trade Flows (WTO) — pre-seed major reporters vs World + key bilateral pairs ───
const BILATERAL_PAIRS = [
['840', '156'], // US ↔ China
['840', '276'], // US ↔ Germany
['840', '392'], // US ↔ Japan
['840', '124'], // US ↔ Canada
['840', '484'], // US ↔ Mexico
['156', '840'], // China ↔ US
['156', '276'], // China ↔ Germany
['826', '156'], // UK ↔ China
['000', '156'], // World ↔ China
['000', '840'], // World ↔ US
];
function parseFlowRows(data, indicator) {
const dataset = Array.isArray(data) ? data : data?.Dataset ?? data?.dataset ?? [];
return dataset.map(row => {
const year = parseInt(row.Year ?? row.year ?? '', 10);
const value = parseFloat(row.Value ?? row.value ?? '');
return !Number.isNaN(year) && !Number.isNaN(value) ? { year, indicator, value } : null;
}).filter(Boolean);
}
function buildFlowRecords(rows, reporterCode, partnerCode) {
const byYear = new Map();
for (const row of rows) {
if (!byYear.has(row.year)) byYear.set(row.year, { exports: 0, imports: 0 });
const e = byYear.get(row.year);
if (row.indicator === 'ITS_MTV_AX') e.exports = row.value; else e.imports = row.value;
}
const sortedYears = [...byYear.keys()].sort((a, b) => a - b);
return sortedYears.map((year, i) => {
const cur = byYear.get(year);
const prev = i > 0 ? byYear.get(sortedYears[i - 1]) : null;
return {
reportingCountry: WTO_MEMBER_CODES[reporterCode] ?? reporterCode,
partnerCountry: partnerCode === '000' ? 'World' : (WTO_MEMBER_CODES[partnerCode] ?? partnerCode),
year, exportValueUsd: cur.exports, importValueUsd: cur.imports,
yoyExportChange: prev?.exports > 0 ? Math.round(((cur.exports - prev.exports) / prev.exports) * 10000) / 100 : 0,
yoyImportChange: prev?.imports > 0 ? Math.round(((cur.imports - prev.imports) / prev.imports) * 10000) / 100 : 0,
productSector: 'Total merchandise',
};
});
}
async function fetchFlowPair(reporter, partner, years, flows) {
const currentYear = new Date().getFullYear();
const startYear = currentYear - years;
const base = { r: reporter, p: partner, ps: `${startYear}-${currentYear}`, pc: 'TO', fmt: 'json', mode: 'full', max: '500' };
const [exportsResult, importsResult] = await Promise.allSettled([
wtoFetch('/data', { ...base, i: 'ITS_MTV_AX' }),
wtoFetch('/data', { ...base, i: 'ITS_MTV_AM' }),
]);
const exportsData = exportsResult.status === 'fulfilled' ? exportsResult.value : null;
const importsData = importsResult.status === 'fulfilled' ? importsResult.value : null;
const rows = [...(exportsData ? parseFlowRows(exportsData, 'ITS_MTV_AX') : []), ...(importsData ? parseFlowRows(importsData, 'ITS_MTV_AM') : [])];
const records = buildFlowRecords(rows, reporter, partner);
const cacheKey = `trade:flows:v1:${reporter}:${partner}:${years}`;
if (records.length > 0) {
flows[cacheKey] = { flows: records, fetchedAt: new Date().toISOString(), upstreamUnavailable: false };
}
}
async function fetchTradeFlows() {
const flows = {};
const years = 10;
for (const reporter of MAJOR_REPORTERS) {
await fetchFlowPair(reporter, '000', years, flows);
await sleep(500);
}
for (const [reporter, partner] of BILATERAL_PAIRS) {
await fetchFlowPair(reporter, partner, years, flows);
await sleep(500);
}
console.log(` Trade flows: ${Object.keys(flows).length} pairs (${MAJOR_REPORTERS.length} world + ${BILATERAL_PAIRS.length} bilateral)`);
return flows;
}
// ─── Trade Barriers (WTO) ───
async function fetchTradeBarriers() {
const currentYear = new Date().getFullYear();
const reporters = MAJOR_REPORTERS.join(',');
const [agriResult, nonAgriResult] = await Promise.allSettled([
wtoFetch('/data', { i: 'TP_A_0160', r: reporters, ps: `${currentYear - 3}-${currentYear}`, fmt: 'json', mode: 'full', max: '500' }),
wtoFetch('/data', { i: 'TP_A_0430', r: reporters, ps: `${currentYear - 3}-${currentYear}`, fmt: 'json', mode: 'full', max: '500' }),
]);
const agriData = agriResult.status === 'fulfilled' ? agriResult.value : null;
const nonAgriData = nonAgriResult.status === 'fulfilled' ? nonAgriResult.value : null;
if (!agriData && !nonAgriData) return null;
const parseRows = (data) => {
const dataset = Array.isArray(data) ? data : data?.Dataset ?? data?.dataset ?? [];
return dataset.map(row => {
const year = parseInt(row.Year ?? row.year ?? '0', 10);
const value = parseFloat(row.Value ?? row.value ?? '');
const cc = String(row.ReportingEconomyCode ?? '');
return !Number.isNaN(year) && !Number.isNaN(value) && cc ? { country: WTO_MEMBER_CODES[cc] ?? '', countryCode: cc, year, value } : null;
}).filter(Boolean);
};
const latestByCountry = (rows) => {
const m = new Map();
for (const r of rows) { const e = m.get(r.countryCode); if (!e || r.year > e.year) m.set(r.countryCode, r); }
return m;
};
const latestAgri = latestByCountry(agriData ? parseRows(agriData) : []);
const latestNonAgri = latestByCountry(nonAgriData ? parseRows(nonAgriData) : []);
const allCodes = new Set([...latestAgri.keys(), ...latestNonAgri.keys()]);
const barriers = [];
for (const code of allCodes) {
const agri = latestAgri.get(code);
const nonAgri = latestNonAgri.get(code);
if (!agri && !nonAgri) continue;
const agriRate = agri?.value ?? 0;
const nonAgriRate = nonAgri?.value ?? 0;
const gap = agriRate - nonAgriRate;
const country = agri?.country ?? nonAgri?.country ?? code;
const year = String(agri?.year ?? nonAgri?.year ?? '');
barriers.push({
id: `${code}-tariff-gap-${year}`, notifyingCountry: country,
title: `Agricultural tariff: ${agriRate.toFixed(1)}% vs Non-agricultural: ${nonAgriRate.toFixed(1)}% (gap: ${gap > 0 ? '+' : ''}${gap.toFixed(1)}pp)`,
measureType: gap > 10 ? 'High agricultural protection' : gap > 5 ? 'Moderate agricultural protection' : 'Low tariff gap',
productDescription: 'Agricultural vs Non-agricultural products',
objective: gap > 0 ? 'Agricultural sector protection' : 'Uniform tariff structure',
status: gap > 10 ? 'high' : gap > 5 ? 'moderate' : 'low',
dateDistributed: year, sourceUrl: 'https://stats.wto.org',
});
}
barriers.sort((a, b) => {
const gapA = parseFloat(a.title.match(/gap: ([+-]?\d+\.?\d*)/)?.[1] ?? '0');
const gapB = parseFloat(b.title.match(/gap: ([+-]?\d+\.?\d*)/)?.[1] ?? '0');
return gapB - gapA;
});
console.log(` Trade barriers: ${barriers.length} countries`);
return { barriers: barriers.slice(0, 50), fetchedAt: new Date().toISOString(), upstreamUnavailable: false };
}
// ─── Trade Restrictions (WTO) ───
async function fetchTradeRestrictions() {
const currentYear = new Date().getFullYear();
const data = await wtoFetch('/data', {
i: 'TP_A_0010', r: MAJOR_REPORTERS.join(','),
ps: `${currentYear - 3}-${currentYear}`, fmt: 'json', mode: 'full', max: '500',
});
if (!data) return null;
const dataset = Array.isArray(data) ? data : data?.Dataset ?? data?.dataset ?? [];
const latestByCountry = new Map();
for (const row of dataset) {
const code = String(row.ReportingEconomyCode ?? '');
const year = parseInt(row.Year ?? row.year ?? '0', 10);
const existing = latestByCountry.get(code);
if (!existing || year > parseInt(existing.Year ?? existing.year ?? '0', 10)) latestByCountry.set(code, row);
}
const restrictions = [...latestByCountry.values()].map(row => {
const value = parseFloat(row.Value ?? row.value ?? '');
if (Number.isNaN(value)) return null;
const cc = String(row.ReportingEconomyCode ?? '');
const year = String(row.Year ?? row.year ?? '');
return {
id: `${cc}-${year}-${row.IndicatorCode ?? ''}`,
reportingCountry: WTO_MEMBER_CODES[cc] ?? String(row.ReportingEconomy ?? ''),
affectedCountry: 'All trading partners', productSector: 'All products',
measureType: 'MFN Applied Tariff', description: `Average tariff rate: ${value.toFixed(1)}%`,
status: value > 10 ? 'high' : value > 5 ? 'moderate' : 'low',
notifiedAt: year, sourceUrl: 'https://stats.wto.org',
};
}).filter(Boolean).sort((a, b) => {
const rateA = parseFloat(a.description.match(/[\d.]+/)?.[0] ?? '0');
const rateB = parseFloat(b.description.match(/[\d.]+/)?.[0] ?? '0');
return rateB - rateA;
}).slice(0, 50);
console.log(` Trade restrictions: ${restrictions.length} countries`);
return { restrictions, fetchedAt: new Date().toISOString(), upstreamUnavailable: false };
}
// ─── Tariff Trends (WTO) — pre-seed major reporters ───
async function fetchTariffTrends() {
const currentYear = new Date().getFullYear();
const trends = {};
for (const reporter of MAJOR_REPORTERS) {
const years = 10;
const data = await wtoFetch('/data', {
i: 'TP_A_0010', r: reporter,
ps: `${currentYear - years}-${currentYear}`, fmt: 'json', mode: 'full', max: '500',
});
if (!data) { await sleep(500); continue; }
const dataset = Array.isArray(data) ? data : data?.Dataset ?? data?.dataset ?? [];
const datapoints = dataset.map(row => {
const year = parseInt(row.Year ?? row.year ?? '', 10);
const tariffRate = parseFloat(row.Value ?? row.value ?? '');
if (Number.isNaN(year) || Number.isNaN(tariffRate)) return null;
return {
reportingCountry: WTO_MEMBER_CODES[reporter] ?? reporter,
partnerCountry: 'World', productSector: 'All products',
year, tariffRate: Math.round(tariffRate * 100) / 100,
boundRate: 0, indicatorCode: 'TP_A_0010',
};
}).filter(Boolean).sort((a, b) => a.year - b.year);
if (datapoints.length > 0) {
const cacheKey = `trade:tariffs:v1:${reporter}:all:${years}`;
trends[cacheKey] = { datapoints, fetchedAt: new Date().toISOString(), upstreamUnavailable: false };
}
await sleep(500);
}
console.log(` Tariff trends: ${Object.keys(trends).length} countries`);
return trends;
}
// ─── US Treasury Customs Revenue ───
const TREASURY_MTS_URL = 'https://api.fiscaldata.treasury.gov/services/api/fiscal_service/v1/accounting/mts/mts_table_9';
async function fetchCustomsRevenue() {
const threeYearsAgo = `${new Date().getFullYear() - 3}-01-01`;
const fields = 'record_date,current_month_rcpt_outly_amt,current_fytd_rcpt_outly_amt,record_fiscal_year,record_calendar_year,record_calendar_month';
const url = `${TREASURY_MTS_URL}?fields=${fields}&filter=classification_desc:eq:Customs%20Duties,record_date:gte:${threeYearsAgo}&sort=-record_date&page[size]=50`;
const resp = await fetch(url, {
headers: { 'User-Agent': CHROME_UA, Accept: 'application/json' },
signal: AbortSignal.timeout(15_000),
});
if (!resp.ok) throw new Error(`Treasury MTS HTTP ${resp.status}`);
const json = await resp.json();
const rows = json.data;
if (!Array.isArray(rows) || rows.length === 0) throw new Error('Treasury MTS returned no data');
if (rows.length > 100) throw new Error(`Treasury MTS returned unexpected row count: ${rows.length}`);
const months = rows
.map(r => {
const monthly = parseFloat(r.current_month_rcpt_outly_amt);
const fytd = parseFloat(r.current_fytd_rcpt_outly_amt);
if (!Number.isFinite(monthly) || !Number.isFinite(fytd)) return null;
return {
recordDate: r.record_date,
fiscalYear: parseInt(r.record_fiscal_year, 10) || 0,
calendarYear: parseInt(r.record_calendar_year, 10) || 0,
calendarMonth: parseInt(r.record_calendar_month, 10) || 0,
monthlyAmountBillions: Math.round((monthly / 1e9) * 100) / 100,
fytdAmountBillions: Math.round((fytd / 1e9) * 100) / 100,
};
})
.filter(Boolean)
.reverse();
console.log(` Treasury customs revenue: ${months.length} months (${months[0]?.recordDate} to ${months[months.length - 1]?.recordDate})`);
return { months, fetchedAt: new Date().toISOString(), upstreamUnavailable: false };
}
// ─── Main ───
async function fetchAll() {
const [shipping, scfi, ccfi, bdi, barriers, restrictions, flows, tariffs, customs] = await Promise.allSettled([
fetchShippingRates(),
fetchSCFI(),
fetchCCFI(),
fetchBDI(),
fetchTradeBarriers(),
fetchTradeRestrictions(),
fetchTradeFlows(),
fetchTariffTrends(),
fetchCustomsRevenue(),
]);
const sh = shipping.status === 'fulfilled' ? shipping.value : null;
const scfiResult = scfi.status === 'fulfilled' ? scfi.value : [];
const ccfiResult = ccfi.status === 'fulfilled' ? ccfi.value : [];
const bdiResult = bdi.status === 'fulfilled' ? bdi.value : [];
const ba = barriers.status === 'fulfilled' ? barriers.value : null;
const re = restrictions.status === 'fulfilled' ? restrictions.value : null;
const fl = flows.status === 'fulfilled' ? flows.value : null;
const ta = tariffs.status === 'fulfilled' ? tariffs.value : null;
const cu = customs.status === 'fulfilled' ? customs.value : null;
if (shipping.status === 'rejected') console.warn(` Shipping failed: ${shipping.reason?.message || shipping.reason}`);
if (scfi.status === 'rejected') console.warn(` SCFI failed: ${scfi.reason?.message || scfi.reason}`);
if (ccfi.status === 'rejected') console.warn(` CCFI failed: ${ccfi.reason?.message || ccfi.reason}`);
if (bdi.status === 'rejected') console.warn(` BDI failed: ${bdi.reason?.message || bdi.reason}`);
if (barriers.status === 'rejected') console.warn(` Barriers failed: ${barriers.reason?.message || barriers.reason}`);
if (restrictions.status === 'rejected') console.warn(` Restrictions failed: ${restrictions.reason?.message || restrictions.reason}`);
if (flows.status === 'rejected') console.warn(` Flows failed: ${flows.reason?.message || flows.reason}`);
if (tariffs.status === 'rejected') console.warn(` Tariffs failed: ${tariffs.reason?.message || tariffs.reason}`);
if (customs.status === 'rejected') console.warn(` Treasury customs failed: ${customs.reason?.message || customs.reason}`);
const allIndices = [
...(sh?.indices || []),
...scfiResult,
...ccfiResult,
...bdiResult,
];
if (allIndices.length === 0 && !ba && !re) throw new Error('All supply-chain/trade fetches failed');
// History accumulation: read previous payload, merge history
let previousPayload = null;
try { previousPayload = await verifySeedKey(KEYS.shipping); } catch { /* ignore */ }
const mergedIndices = accumulateHistory(allIndices, previousPayload);
console.log(` Merged shipping indices: ${mergedIndices.length} (FRED: ${sh?.indices?.length ?? 0}, SCFI: ${scfiResult.length}, CCFI: ${ccfiResult.length}, BDI: ${bdiResult.length})`);
// Write secondary keys BEFORE returning (runSeed calls process.exit after primary write)
if (ba) await writeExtraKeyWithMeta(KEYS.barriers, ba, TRADE_TTL, ba.barriers?.length ?? 0);
if (re) await writeExtraKeyWithMeta(KEYS.restrictions, re, TRADE_TTL, re.restrictions?.length ?? 0);
if (fl) { for (const [key, data] of Object.entries(fl)) await writeExtraKeyWithMeta(key, data, TRADE_TTL, data.flows?.length ?? 0); }
if (ta) { for (const [key, data] of Object.entries(ta)) await writeExtraKeyWithMeta(key, data, TRADE_TTL, data.datapoints?.length ?? 0); }
if (cu) await writeExtraKeyWithMeta(KEYS.customsRevenue, cu, TRADE_TTL, cu.months?.length ?? 0);
return mergedIndices.length > 0
? { indices: mergedIndices, fetchedAt: new Date().toISOString(), upstreamUnavailable: false }
: { indices: [], fetchedAt: new Date().toISOString(), upstreamUnavailable: true };
}
function validate(data) {
return data?.indices?.length > 0;
}
runSeed('supply_chain', 'shipping', KEYS.shipping, fetchAll, {
validateFn: validate,
ttlSeconds: SHIPPING_TTL,
sourceVersion: 'fred-wto-sse-bdi',
}).catch((err) => {
const _cause = err.cause ? ` (cause: ${err.cause.message || err.cause.code || err.cause})` : ''; console.error('FATAL:', (err.message || err) + _cause);
process.exit(1);
});