Files
OpenSquawk/app/composables/useApi.ts
leubeem 1ae985fdc2 fix: interrupt ATC speech on frequency change, fix PTT stack overflow
- Stop in-progress ATC speech (HTTP request + audio playback) when the
  active frequency changes via preset select or swap button. Prevents the
  pilot "hearing" a controller on a frequency they have already left.
- Fix RangeError crash for long PTT recordings: btoa(String.fromCharCode
  (...largeArray)) overflows the call stack above ~2 s of audio and
  silently drops the PTT request. Replaced with chunked 8 KB conversion.
- Add 30 s PTT auto-stop timer so runaway holds are submitted rather than
  silently lost.
- Add AbortSignal support to useApi so fetch requests can be cancelled.
2026-06-08 10:14:07 +02:00

74 lines
2.3 KiB
TypeScript

import { useAuthStore } from '~/stores/auth'
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
interface ApiRequestOptions<T = any> {
method?: HttpMethod
body?: T
query?: Record<string, any>
headers?: HeadersInit
auth?: boolean
/** AbortSignal — pass to cancel an in-flight request (e.g. on frequency change). */
signal?: AbortSignal
}
export function useApi() {
const auth = useAuthStore()
const execute = async <T>(path: string, options: ApiRequestOptions = {}) => {
const { method = 'GET', body, query, headers = {}, auth: requiresAuth = true, signal } = options
const computedHeaders: Record<string, string> = {
Accept: 'application/json',
...(headers as Record<string, string>),
}
if (requiresAuth && auth.accessToken) {
computedHeaders.Authorization = `Bearer ${auth.accessToken}`
}
const requestOptions: any = {
method,
headers: computedHeaders,
query,
body,
signal,
}
if (body && !(body instanceof FormData)) {
computedHeaders['Content-Type'] = 'application/json'
requestOptions.body = body
}
try {
return await $fetch<T>(path, requestOptions)
} catch (error: any) {
// Propagate abort errors immediately — do not retry on 401.
if (error?.name === 'AbortError' || signal?.aborted) throw error
const status = error?.status || error?.response?.status
if (status === 401 && requiresAuth) {
const refreshed = await auth.tryRefresh()
if (refreshed) {
if (auth.accessToken) {
computedHeaders.Authorization = `Bearer ${auth.accessToken}`
}
return await $fetch<T>(path, { ...requestOptions, signal })
}
await auth.logout()
}
throw error
}
}
return {
request: execute,
get: <T>(path: string, options: ApiRequestOptions = {}) => execute<T>(path, { ...options, method: 'GET' }),
post: <T>(path: string, body?: any, options: ApiRequestOptions = {}) =>
execute<T>(path, { ...options, method: 'POST', body }),
put: <T>(path: string, body?: any, options: ApiRequestOptions = {}) =>
execute<T>(path, { ...options, method: 'PUT', body }),
del: <T>(path: string, options: ApiRequestOptions = {}) => execute<T>(path, { ...options, method: 'DELETE' }),
}
}