| import { getRpcBaseUrl } from '@/services/rpc-client'; |
| import { premiumFetch } from '@/services/premium-fetch'; |
| import type { CargoType } from '@/config/bypass-corridors'; |
| import type { GetShippingRatesResponse, GetChokepointStatusResponse, GetChokepointHistoryResponse, GetCriticalMineralsResponse, GetShippingStressResponse, GetCountryChokepointIndexResponse, GetBypassOptionsResponse, GetCountryCostShockResponse, GetCountryProductsResponse, GetMultiSectorCostShockResponse, GetSectorDependencyResponse, GetRouteExplorerLaneResponse, GetRouteImpactResponse, ShippingIndex, ChokepointInfo, CriticalMineral, MineralProducer, ShippingRatePoint, ChokepointExposureEntry, BypassOption, TransitDayCount, CountryProduct, ProductExporter, MultiSectorCostShock } from '@/generated/client/worldmonitor/supply_chain/v1/service_client'; |
| import { createCircuitBreaker } from '@/utils'; |
| import { getHydratedData } from '@/services/bootstrap'; |
| import { hasPremiumAccess } from '@/services/panel-gating'; |
| import { SupplyChainServiceClient } from '@/services/generated-rpc-clients'; |
| import { |
| type ChinaCorridorControlTowerResponse, |
| } from '../../../shared/china-corridor-control-towers'; |
| import { |
| CHINA_CORRIDOR_BREAKER_CACHE_POLICY, |
| fetchChinaCorridorControlTowers as fetchChinaCorridorControlTowersWithDependencies, |
| } from './china-corridor-control-towers'; |
|
|
| export { parseChinaCorridorResponse } from './china-corridor-control-towers'; |
|
|
| export type { |
| ChinaCorridorCondition, |
| ChinaCorridorControlTower, |
| ChinaCorridorControlTowerResponse, |
| CorridorAvailability, |
| CorridorSourceSignal, |
| } from '../../../shared/china-corridor-control-towers'; |
|
|
| export type { |
| GetShippingRatesResponse, |
| GetChokepointStatusResponse, |
| GetChokepointHistoryResponse, |
| GetCriticalMineralsResponse, |
| GetShippingStressResponse, |
| GetCountryChokepointIndexResponse, |
| GetBypassOptionsResponse, |
| GetCountryCostShockResponse, |
| GetCountryProductsResponse, |
| GetMultiSectorCostShockResponse, |
| GetSectorDependencyResponse, |
| GetRouteExplorerLaneResponse, |
| GetRouteImpactResponse, |
| ShippingIndex, |
| ChokepointInfo, |
| CriticalMineral, |
| MineralProducer, |
| ShippingRatePoint, |
| ChokepointExposureEntry, |
| BypassOption, |
| TransitDayCount, |
| CountryProduct, |
| ProductExporter, |
| MultiSectorCostShock, |
| }; |
|
|
| |
| |
| export type CountryProductsResponse = GetCountryProductsResponse; |
| export type MultiSectorShockResponse = GetMultiSectorCostShockResponse; |
| export type MultiSectorShock = MultiSectorCostShock; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| const client = new SupplyChainServiceClient(getRpcBaseUrl(), { fetch: premiumFetch }); |
|
|
| const shippingBreaker = createCircuitBreaker<GetShippingRatesResponse>({ name: 'Shipping Rates', cacheTtlMs: 60 * 60 * 1000, persistCache: true }); |
| const chokepointBreaker = createCircuitBreaker<GetChokepointStatusResponse>({ name: 'Chokepoint Status', cacheTtlMs: 90 * 60 * 1000, persistCache: true }); |
| const mineralsBreaker = createCircuitBreaker<GetCriticalMineralsResponse>({ name: 'Critical Minerals', cacheTtlMs: 24 * 60 * 60 * 1000, persistCache: true }); |
| const chinaCorridorBreaker = createCircuitBreaker<ChinaCorridorControlTowerResponse>({ |
| name: 'China Corridor Control Towers', |
| ...CHINA_CORRIDOR_BREAKER_CACHE_POLICY, |
| }); |
|
|
| const emptyShipping: GetShippingRatesResponse = { indices: [], fetchedAt: '', upstreamUnavailable: false }; |
| const emptyChokepoints: GetChokepointStatusResponse = { chokepoints: [], fetchedAt: '', upstreamUnavailable: false }; |
| const emptyMinerals: GetCriticalMineralsResponse = { minerals: [], fetchedAt: '', upstreamUnavailable: false }; |
|
|
| export async function fetchChinaCorridorControlTowers(): Promise<ChinaCorridorControlTowerResponse> { |
| return fetchChinaCorridorControlTowersWithDependencies({ |
| now: () => new Date(), |
| getResponse: () => client.getChinaCorridorControlTowers({}), |
| execute: (operation, fallback) => |
| chinaCorridorBreaker.execute(operation, fallback), |
| }); |
| } |
|
|
| export async function fetchShippingRates(): Promise<GetShippingRatesResponse> { |
| const hydrated = getHydratedData('shippingRates') as GetShippingRatesResponse | undefined; |
| if (hydrated?.indices?.length) return hydrated; |
|
|
| try { |
| return await shippingBreaker.execute(async () => { |
| return client.getShippingRates({}); |
| }, emptyShipping); |
| } catch { |
| return emptyShipping; |
| } |
| } |
|
|
| export async function fetchChokepointStatus(): Promise<GetChokepointStatusResponse> { |
| const hydrated = getHydratedData('chokepoints') as GetChokepointStatusResponse | undefined; |
| if (hydrated?.chokepoints?.length) return hydrated; |
|
|
| try { |
| return await chokepointBreaker.execute(async () => { |
| return client.getChokepointStatus({}); |
| }, emptyChokepoints); |
| } catch { |
| return emptyChokepoints; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function fetchChokepointHistory( |
| chokepointId: string, |
| ): Promise<GetChokepointHistoryResponse> { |
| try { |
| return await client.getChokepointHistory({ chokepointId }); |
| } catch { |
| return { chokepointId, history: [], fetchedAt: '0' }; |
| } |
| } |
|
|
| export async function fetchCriticalMinerals(): Promise<GetCriticalMineralsResponse> { |
| const hydrated = getHydratedData('minerals') as GetCriticalMineralsResponse | undefined; |
| if (hydrated?.minerals?.length) return hydrated; |
|
|
| try { |
| return await mineralsBreaker.execute(async () => { |
| return client.getCriticalMinerals({}); |
| }, emptyMinerals); |
| } catch { |
| return emptyMinerals; |
| } |
| } |
|
|
| const emptyShippingStress: GetShippingStressResponse = { carriers: [], stressScore: 0, stressLevel: 'low', fetchedAt: 0, upstreamUnavailable: false }; |
|
|
| export async function fetchShippingStress(): Promise<GetShippingStressResponse> { |
| const hydrated = getHydratedData('shippingStress') as GetShippingStressResponse | undefined; |
| if (hydrated?.carriers?.length) return hydrated; |
|
|
| try { |
| return await client.getShippingStress({}); |
| } catch { |
| return emptyShippingStress; |
| } |
| } |
|
|
| const emptyChokepointIndex: GetCountryChokepointIndexResponse = { |
| iso2: '', |
| hs2: '27', |
| exposures: [], |
| primaryChokepointId: '', |
| vulnerabilityIndex: 0, |
| fetchedAt: '', |
| }; |
|
|
| export async function fetchCountryChokepointIndex( |
| iso2: string, |
| hs2 = '27', |
| ): Promise<GetCountryChokepointIndexResponse> { |
| |
| |
| |
| |
| |
| if (!hasPremiumAccess()) return { ...emptyChokepointIndex, iso2, hs2 }; |
| try { |
| return await client.getCountryChokepointIndex({ iso2, hs2 }); |
| } catch { |
| return { ...emptyChokepointIndex, iso2, hs2 }; |
| } |
| } |
|
|
| |
| export const SEEDED_HS2_CODES = ['27', '84', '85', '87', '30', '72', '39', '29', '10', '62'] as const; |
|
|
| |
| export const HS2_SHORT_LABELS: Record<string, string> = { |
| '27': 'Energy', '84': 'Machinery', '85': 'Electronics', '87': 'Vehicles', |
| '30': 'Pharma', '72': 'Iron & Steel', '39': 'Plastics', '29': 'Chemicals', |
| '10': 'Cereals', '62': 'Apparel', |
| }; |
|
|
| export interface SectorExposureSummary { |
| hs2: string; |
| label: string; |
| primaryChokepointId: string; |
| primaryChokepointName: string; |
| exposureScore: number; |
| vulnerabilityIndex: number; |
| dependencyFlag: string; |
| primaryExporterIso2: string; |
| primaryExporterShare: number; |
| fetchedAt?: string; |
| } |
|
|
| |
| |
| |
| |
| export async function fetchMultiSectorExposure(iso2: string): Promise<SectorExposureSummary[]> { |
| const exposureResults = await Promise.all( |
| SEEDED_HS2_CODES.map(hs2 => fetchCountryChokepointIndex(iso2, hs2)), |
| ); |
| const activeCodes = exposureResults.filter(r => r.exposures.length > 0).map(r => r.hs2); |
| const depResults = activeCodes.length > 0 |
| ? await Promise.all(activeCodes.map(hs2 => fetchSectorDependency(iso2, hs2))) |
| : []; |
|
|
| const depMap = new Map(depResults.map(d => [d.hs2, d])); |
|
|
| return exposureResults |
| .filter(r => r.exposures.length > 0) |
| .map(r => { |
| const dep = depMap.get(r.hs2); |
| return { |
| hs2: r.hs2, |
| label: HS2_SHORT_LABELS[r.hs2] ?? r.hs2, |
| primaryChokepointId: r.primaryChokepointId, |
| primaryChokepointName: r.exposures[0]?.chokepointName ?? r.primaryChokepointId, |
| exposureScore: r.exposures[0]?.exposureScore ?? 0, |
| vulnerabilityIndex: r.vulnerabilityIndex, |
| dependencyFlag: dep?.flags?.[0] ?? '', |
| primaryExporterIso2: dep?.primaryExporterIso2 ?? '', |
| primaryExporterShare: dep?.primaryExporterShare ?? 0, |
| fetchedAt: r.fetchedAt, |
| }; |
| }) |
| .sort((a, b) => b.vulnerabilityIndex - a.vulnerabilityIndex); |
| } |
|
|
| export async function fetchBypassOptions( |
| chokepointId: string, |
| cargoType: CargoType = 'container', |
| closurePct = 100, |
| ): Promise<GetBypassOptionsResponse> { |
| const empty: GetBypassOptionsResponse = { chokepointId, cargoType, closurePct, options: [], primaryChokepointWarRiskTier: 'WAR_RISK_TIER_UNSPECIFIED', fetchedAt: '' }; |
| |
| if (!hasPremiumAccess()) return empty; |
| try { |
| return await client.getBypassOptions({ chokepointId, cargoType, closurePct }); |
| } catch { |
| return empty; |
| } |
| } |
|
|
| export async function fetchCountryCostShock( |
| iso2: string, |
| chokepointId: string, |
| hs2 = '27', |
| ): Promise<GetCountryCostShockResponse> { |
| const empty: GetCountryCostShockResponse = { |
| iso2, chokepointId, hs2, |
| supplyDeficitPct: 0, coverageDays: 0, warRiskPremiumBps: 0, |
| warRiskTier: 'WAR_RISK_TIER_UNSPECIFIED', |
| hasEnergyModel: false, unavailableReason: '', fetchedAt: '', |
| }; |
| |
| if (!hasPremiumAccess()) return empty; |
| try { |
| return await client.getCountryCostShock({ iso2, chokepointId, hs2 }); |
| } catch { |
| return empty; |
| } |
| } |
|
|
| const emptySectorDependency: GetSectorDependencyResponse = { |
| iso2: '', hs2: '27', hs2Label: '', flags: [], |
| primaryExporterIso2: '', primaryExporterShare: 0, |
| primaryChokepointId: '', primaryChokepointExposure: 0, |
| hasViableBypass: false, fetchedAt: '', |
| }; |
|
|
| export async function fetchSectorDependency( |
| iso2: string, |
| hs2 = '27', |
| ): Promise<GetSectorDependencyResponse> { |
| |
| if (!hasPremiumAccess()) return { ...emptySectorDependency, iso2, hs2 }; |
| try { |
| return await client.getSectorDependency({ iso2, hs2 }); |
| } catch { |
| return { ...emptySectorDependency, iso2, hs2 }; |
| } |
| } |
|
|
| const emptyRouteExplorerLane: GetRouteExplorerLaneResponse = { |
| fromIso2: '', toIso2: '', hs2: '', cargoType: '', |
| primaryRouteId: '', |
| primaryRouteGeometry: [], |
| chokepointExposures: [], |
| bypassOptions: [], |
| warRiskTier: 'WAR_RISK_TIER_NORMAL', |
| disruptionScore: 0, |
| noModeledLane: true, |
| fetchedAt: '', |
| }; |
|
|
| export interface FetchRouteExplorerLaneArgs { |
| fromIso2: string; |
| toIso2: string; |
| hs2: string; |
| cargoType: string; |
| } |
|
|
| export async function fetchRouteExplorerLane( |
| args: FetchRouteExplorerLaneArgs, |
| ): Promise<GetRouteExplorerLaneResponse> { |
| |
| if (!hasPremiumAccess()) return { ...emptyRouteExplorerLane, ...args }; |
| try { |
| return await client.getRouteExplorerLane(args); |
| } catch { |
| return { ...emptyRouteExplorerLane, ...args }; |
| } |
| } |
|
|
| const emptyRouteImpact: GetRouteImpactResponse = { |
| laneValueUsd: 0, |
| primaryExporterIso2: '', |
| primaryExporterShare: 0, |
| topStrategicProducts: [], |
| resilienceScore: 0, |
| dependencyFlags: [], |
| hs2InSeededUniverse: false, |
| comtradeSource: 'missing', |
| fetchedAt: '', |
| }; |
|
|
| export interface FetchRouteImpactArgs { |
| fromIso2: string; |
| toIso2: string; |
| hs2: string; |
| } |
|
|
| export async function fetchRouteImpact( |
| args: FetchRouteImpactArgs, |
| ): Promise<GetRouteImpactResponse> { |
| |
| if (!hasPremiumAccess()) return { ...emptyRouteImpact }; |
| try { |
| return await client.getRouteImpact(args); |
| } catch { |
| return { ...emptyRouteImpact }; |
| } |
| } |
|
|
| const emptyProducts: GetCountryProductsResponse = { iso2: '', products: [], fetchedAt: '' }; |
|
|
| export async function fetchCountryProducts(iso2: string): Promise<GetCountryProductsResponse> { |
| |
| if (!hasPremiumAccess()) return { ...emptyProducts, iso2 }; |
| try { |
| return await client.getCountryProducts({ iso2 }); |
| } catch { |
| return { ...emptyProducts, iso2 }; |
| } |
| } |
|
|
| const emptyMultiSectorShock: GetMultiSectorCostShockResponse = { |
| iso2: '', |
| chokepointId: '', |
| closureDays: 30, |
| warRiskTier: 'WAR_RISK_TIER_UNSPECIFIED', |
| sectors: [], |
| totalAddedCost: 0, |
| fetchedAt: '', |
| unavailableReason: '', |
| }; |
|
|
| |
| |
| |
| |
| export async function fetchMultiSectorCostShock( |
| iso2: string, |
| chokepointId: string, |
| closureDays: number, |
| options?: { signal?: AbortSignal }, |
| ): Promise<GetMultiSectorCostShockResponse> { |
| |
| |
| |
| if (!hasPremiumAccess()) return { ...emptyMultiSectorShock, iso2, chokepointId, closureDays }; |
| try { |
| return await client.getMultiSectorCostShock( |
| { iso2, chokepointId, closureDays }, |
| { signal: options?.signal }, |
| ); |
| } catch { |
| return { ...emptyMultiSectorShock, iso2, chokepointId, closureDays }; |
| } |
| } |
|
|