src/blackrock/api.ts

import {useEffect,useState} from 'react';
export const BASE='/api/blackrock/v1';
export type Relationship='owned'|'ally'|'other'|'unclassified';
export const relationships:Record<Relationship,string>={owned:'My bots',ally:'Allies',other:'Other players',unclassified:'Unclassified'};
export type Entity={kind:'item'|'player';key:string;name:string;itemId?:number;view?:'overview'|'orders'|'holders'|'charts'|'production'|'history';saved?:boolean;friendly?:boolean;source?:string;aliases?:string[];teamNames?:string[];relationship?:Relationship};
export function unwrapApiPayload(payload:any){return payload?.schemaVersion===1&&Object.prototype.hasOwnProperty.call(payload,'data')?payload.data:payload;}
export class ApiError extends Error {constructor(message:string,public status:number,public code?:string){super(message)}}
export async function api<T=any>(path:string,options:RequestInit={}):Promise<T>{
 const response=await fetch(path.startsWith('/api/')?path:BASE+path,{...options,headers:{...options.body?{'Content-Type':'application/json'}:{},...options.headers},signal:options.signal??AbortSignal.timeout(125000)});
 const payload=await response.json().catch(()=>null);
 if(!response.ok||payload?.ok===false||payload?.error&&!payload?.data)throw new ApiError(typeof payload?.error==='string'?payload.error:payload?.error?.message??`Request failed (${response.status})`,response.status,payload?.error?.code);
 return unwrapApiPayload(payload);
}
export const mutate=(path:string,method:string,body?:unknown)=>api(path,{method,...body===undefined?{}:{body:JSON.stringify(body)}});
export function useResource<T=any>(path:string|null,revision=0,interval=0){
 const [state,setState]=useState<{data:T|null;error:string|null;loading:boolean}>({data:null,error:null,loading:!!path});
 useEffect(()=>{if(!path){setState({data:null,error:null,loading:false});return;} let alive=true;let running=false;const controller=new AbortController();setState({data:null,error:null,loading:true});
 const run=async()=>{if(running)return;running=true;try{const data=await api<T>(path,{signal:controller.signal});if(alive)setState({data,error:null,loading:false});}catch(e){if(alive)setState(s=>({...s,error:(e as Error).message,loading:false}));}finally{running=false;}};
 void run();const timer=interval?setInterval(run,interval):null;return()=>{alive=false;controller.abort();if(timer)clearInterval(timer);};
 },[path,revision,interval]);return state;
}
export function number(value:unknown){return typeof value==='number'&&Number.isFinite(value)?new Intl.NumberFormat(undefined,{maximumFractionDigits:2}).format(value):'Not observed'}
export function time(value:unknown){if(value===null||value===undefined||value==='')return 'Unknown';const date=new Date(value as number|string);return Number.isNaN(date.getTime())?'Unknown':date.toLocaleString()}
export function download(name:string,data:unknown){const url=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'}));const a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000)}