import type { Manifest } from '@types' import { buildAppUrl } from '@urls' import { AsyncLocalStorage } from 'node:async_hooks' const DEFAULT_HOST = process.env.DEV ? 'http://localhost:3000' : 'http://toes.local' const signalStore = new AsyncLocalStorage() const normalizeUrl = (url: string) => url.startsWith('http://') || url.startsWith('https://') ? url : `http://${url}` function tryParseError(text: string): string | undefined { try { const json = JSON.parse(text) return json.error } catch { return undefined } } export const HOST = process.env.TOES_URL ? normalizeUrl(process.env.TOES_URL) : DEFAULT_HOST export const gitUrl = (name: string) => `${buildAppUrl('git', HOST)}/${name}` export const getSignal = () => signalStore.getStore() export function withSignal(signal: AbortSignal, fn: () => T): T { return signalStore.run(signal, fn) } export function makeUrl(path: string): string { return `${HOST}${path}` } export function handleError(error: unknown): void { if (error instanceof Error && 'code' in error && error.code === 'ConnectionRefused') { console.error(`🐾 Can't connect to toes server at ${HOST}`) console.error(` Set TOES_URL to connect to a different host`) return } if (error instanceof Error) { console.error(`Error: ${error.message}`) return } console.error(error) } export async function get(url: string): Promise { try { const res = await fetch(makeUrl(url), { signal: getSignal() }) if (!res.ok) { const text = await res.text() const msg = tryParseError(text) ?? `${res.status} ${res.statusText}` throw new Error(msg) } return await res.json() } catch (error) { handleError(error) } } export async function getManifest(appName: string): Promise<{ exists: boolean, manifest?: Manifest } | null> { try { const res = await fetch(makeUrl(`/api/sync/apps/${appName}/manifest`), { signal: getSignal() }) if (res.status === 404) return { exists: false } if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) const manifest = await res.json() return { exists: true, manifest } } catch (error) { handleError(error) return null } } export async function post(url: string, body?: B): Promise { try { const res = await fetch(makeUrl(url), { method: 'POST', headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined, body: body !== undefined ? JSON.stringify(body) : undefined, signal: getSignal(), }) if (!res.ok) { const text = await res.text() const msg = tryParseError(text) ?? `${res.status} ${res.statusText}` throw new Error(msg) } return await res.json() } catch (error) { handleError(error) } } export async function put(url: string, body: Buffer | Uint8Array): Promise { try { const res = await fetch(makeUrl(url), { method: 'PUT', body: body as BodyInit, signal: getSignal(), }) if (!res.ok) { const text = await res.text() const msg = tryParseError(text) ?? `${res.status} ${res.statusText}` throw new Error(msg) } return true } catch (error) { handleError(error) return false } } export async function download(url: string): Promise { try { const fullUrl = makeUrl(url) const res = await fetch(fullUrl, { signal: getSignal() }) if (!res.ok) { const text = await res.text() const msg = tryParseError(text) ?? `${res.status} ${res.statusText}` throw new Error(msg) } return Buffer.from(await res.arrayBuffer()) } catch (error) { handleError(error) } } export async function del(url: string): Promise { try { const res = await fetch(makeUrl(url), { method: 'DELETE', signal: getSignal(), }) if (!res.ok) { const text = await res.text() const msg = tryParseError(text) ?? `${res.status} ${res.statusText}` throw new Error(msg) } return true } catch (error) { handleError(error) return false } }