146 lines
4.1 KiB
TypeScript
146 lines
4.1 KiB
TypeScript
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<AbortSignal>()
|
|
|
|
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<T>(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<T>(url: string): Promise<T | undefined> {
|
|
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<T, B = unknown>(url: string, body?: B): Promise<T | undefined> {
|
|
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<boolean> {
|
|
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<Buffer | undefined> {
|
|
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<boolean> {
|
|
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
|
|
}
|
|
}
|