mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-01 06:06:05 +08:00
- 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.
74 lines
2.3 KiB
TypeScript
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' }),
|
|
}
|
|
}
|
|
|