| |
| |
| |
| |
|
|
| import { getRpcBaseUrl } from '@/services/rpc-client'; |
| import { premiumFetch } from '@/services/premium-fetch'; |
| import { getCurrentClerkUser } from '@/services/clerk'; |
| import { hasPremiumAccess } from '@/services/panel-gating'; |
| import { onEntitlementChange } from '@/services/entitlements'; |
| import { IS_EMBEDDED_PREVIEW } from '@/utils/embedded-preview'; |
| import type { GetTradeRestrictionsResponse, GetTariffTrendsResponse, GetTradeFlowsResponse, GetTradeBarriersResponse, GetCustomsRevenueResponse, ListComtradeFlowsResponse, ComtradeFlowRecord, TradeRestriction, TariffDataPoint, EffectiveTariffRate, TradeFlowRecord, TradeBarrier, CustomsRevenueMonth } from '@/generated/client/worldmonitor/trade/v1/service_client'; |
| import { createCircuitBreaker } from '@/utils'; |
| import { isFeatureAvailable } from '../runtime-config'; |
| import { getHydratedData } from '@/services/bootstrap'; |
| import { TradeServiceClient } from '@/services/generated-rpc-clients'; |
|
|
| |
| export type { TradeRestriction, TariffDataPoint, EffectiveTariffRate, TradeFlowRecord, TradeBarrier, CustomsRevenueMonth, ComtradeFlowRecord }; |
| export type { |
| GetTradeRestrictionsResponse, |
| GetTariffTrendsResponse, |
| GetTradeFlowsResponse, |
| GetTradeBarriersResponse, |
| GetCustomsRevenueResponse, |
| ListComtradeFlowsResponse, |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const publicClient = new TradeServiceClient(getRpcBaseUrl(), { fetch: (...args) => globalThis.fetch(...args) }); |
| const premiumClient = new TradeServiceClient(getRpcBaseUrl(), { fetch: premiumFetch }); |
|
|
| const restrictionsBreaker = createCircuitBreaker<GetTradeRestrictionsResponse>({ name: 'WTO Restrictions', cacheTtlMs: 30 * 60 * 1000, persistCache: true }); |
| |
| |
| |
| const tariffsBreaker = createCircuitBreaker<GetTariffTrendsResponse>({ name: 'WTO Tariffs', cacheTtlMs: 30 * 60 * 1000, persistCache: false }); |
| const flowsBreaker = createCircuitBreaker<GetTradeFlowsResponse>({ name: 'WTO Flows', cacheTtlMs: 30 * 60 * 1000, persistCache: true }); |
| const barriersBreaker = createCircuitBreaker<GetTradeBarriersResponse>({ name: 'WTO Barriers', cacheTtlMs: 30 * 60 * 1000, persistCache: true }); |
| const revenueBreaker = createCircuitBreaker<GetCustomsRevenueResponse>({ name: 'Treasury Revenue', cacheTtlMs: 30 * 60 * 1000, persistCache: true }); |
| const comtradeBreaker = createCircuitBreaker<ListComtradeFlowsResponse>({ name: 'Comtrade Flows', cacheTtlMs: 6 * 60 * 60 * 1000, persistCache: false }); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| let lastPremiumFingerprint: string | null | undefined; |
|
|
| function currentPremiumFingerprint(): string { |
| let userId = 'anon'; |
| try { |
| userId = getCurrentClerkUser()?.id ?? 'anon'; |
| } catch { } |
| let entitled = false; |
| try { |
| entitled = hasPremiumAccess(); |
| } catch { } |
| return `${userId}:${entitled ? 'pro' : 'free'}`; |
| } |
|
|
| function invalidatePremiumBreakersIfIdentityChanged(): void { |
| const fp = currentPremiumFingerprint(); |
| if (lastPremiumFingerprint !== undefined && fp !== lastPremiumFingerprint) { |
| tariffsBreaker.clearMemoryCache(); |
| comtradeBreaker.clearMemoryCache(); |
| } |
| lastPremiumFingerprint = fp; |
| } |
|
|
| |
| |
| |
| |
| |
| onEntitlementChange(() => { |
| const fp = currentPremiumFingerprint(); |
| if (lastPremiumFingerprint !== undefined && fp !== lastPremiumFingerprint) { |
| tariffsBreaker.clearMemoryCache(); |
| comtradeBreaker.clearMemoryCache(); |
| lastPremiumFingerprint = fp; |
| } |
| }); |
|
|
| const emptyRestrictions: GetTradeRestrictionsResponse = { restrictions: [], fetchedAt: '', upstreamUnavailable: false }; |
| const emptyTariffs: GetTariffTrendsResponse = { datapoints: [], fetchedAt: '', upstreamUnavailable: false }; |
| const emptyFlows: GetTradeFlowsResponse = { flows: [], fetchedAt: '', upstreamUnavailable: false }; |
| const emptyBarriers: GetTradeBarriersResponse = { barriers: [], fetchedAt: '', upstreamUnavailable: false }; |
| const emptyRevenue: GetCustomsRevenueResponse = { months: [], fetchedAt: '', upstreamUnavailable: false }; |
| const emptyComtrade: ListComtradeFlowsResponse = { flows: [], fetchedAt: '', upstreamUnavailable: false }; |
|
|
| export async function fetchTradeRestrictions(countries: string[] = [], limit = 50): Promise<GetTradeRestrictionsResponse> { |
| if (!isFeatureAvailable('wtoTrade')) return emptyRestrictions; |
| try { |
| return await restrictionsBreaker.execute(async () => { |
| return publicClient.getTradeRestrictions({ countries, limit }); |
| }, emptyRestrictions, { shouldCache: r => (r.restrictions?.length ?? 0) > 0 }); |
| } catch { |
| return emptyRestrictions; |
| } |
| } |
|
|
| export async function fetchTariffTrends(reportingCountry: string, partnerCountry: string, productSector = '', years = 10): Promise<GetTariffTrendsResponse> { |
| if (!isFeatureAvailable('wtoTrade')) return emptyTariffs; |
| |
| |
| |
| if (IS_EMBEDDED_PREVIEW) return emptyTariffs; |
| invalidatePremiumBreakersIfIdentityChanged(); |
| try { |
| return await tariffsBreaker.execute(async () => { |
| return premiumClient.getTariffTrends({ reportingCountry, partnerCountry, productSector, years }); |
| }, emptyTariffs, { shouldCache: r => (r.datapoints?.length ?? 0) > 0 }); |
| } catch { |
| return emptyTariffs; |
| } |
| } |
|
|
| export async function fetchTradeFlows(reportingCountry: string, partnerCountry: string, years = 10): Promise<GetTradeFlowsResponse> { |
| if (!isFeatureAvailable('wtoTrade')) return emptyFlows; |
| try { |
| return await flowsBreaker.execute(async () => { |
| return publicClient.getTradeFlows({ reportingCountry, partnerCountry, years }); |
| }, emptyFlows, { shouldCache: r => (r.flows?.length ?? 0) > 0 }); |
| } catch { |
| return emptyFlows; |
| } |
| } |
|
|
| export async function fetchTradeBarriers(countries: string[] = [], measureType = '', limit = 50): Promise<GetTradeBarriersResponse> { |
| if (!isFeatureAvailable('wtoTrade')) return emptyBarriers; |
| try { |
| return await barriersBreaker.execute(async () => { |
| return publicClient.getTradeBarriers({ countries, measureType, limit }); |
| }, emptyBarriers, { shouldCache: r => (r.barriers?.length ?? 0) > 0 }); |
| } catch { |
| return emptyBarriers; |
| } |
| } |
|
|
| export async function fetchCustomsRevenue(): Promise<GetCustomsRevenueResponse> { |
| const hydrated = getHydratedData('customsRevenue') as GetCustomsRevenueResponse | undefined; |
| if (hydrated?.months?.length) return hydrated; |
| try { |
| return await revenueBreaker.execute(async () => { |
| return publicClient.getCustomsRevenue({}); |
| }, emptyRevenue, { shouldCache: r => (r.months?.length ?? 0) > 0 }); |
| } catch { |
| return emptyRevenue; |
| } |
| } |
|
|
| export async function fetchComtradeFlows(): Promise<ListComtradeFlowsResponse> { |
| |
| if (IS_EMBEDDED_PREVIEW) return emptyComtrade; |
| invalidatePremiumBreakersIfIdentityChanged(); |
| try { |
| return await comtradeBreaker.execute(async () => { |
| return premiumClient.listComtradeFlows({ reporterCode: '', cmdCode: '', anomaliesOnly: false }); |
| }, emptyComtrade, { shouldCache: r => (r.flows?.length ?? 0) > 0 }); |
| } catch { |
| return emptyComtrade; |
| } |
| } |
|
|