Implement HTTP timeout configurations and enhance API documentation

- Added optional HTTP server timeout configurations (`HNHMAP_READ_TIMEOUT`, `HNHMAP_WRITE_TIMEOUT`, `HNHMAP_IDLE_TIMEOUT`) to `.env.example` and updated the server initialization in `main.go` to utilize these settings.
- Enhanced API documentation for the `rebuildZooms` endpoint to clarify its background processing and polling mechanism for status updates.
- Updated `configuration.md` to include new timeout environment variables for better configuration guidance.
- Improved error handling in the client for large request bodies, ensuring appropriate responses for oversized payloads.
This commit is contained in:
2026-03-04 11:59:28 +03:00
parent a3a4c0e896
commit dda35baeca
17 changed files with 396 additions and 73 deletions

View File

@@ -1,8 +1,11 @@
import md5 from 'md5'
const gravatarCache = new Map<string, string>()
/**
* Returns Gravatar avatar URL for the given email, or empty string if no email.
* Gravatar expects: trim, lowercase, then MD5 hex. Use empty string to show a placeholder (e.g. initial letter) in the UI.
* Results are memoized by (email, size) to avoid recomputing MD5 on every render.
*
* @param email - User email (optional)
* @param size - Avatar size in pixels (default 64; navbar 32, drawer 40, profile 6480)
@@ -10,7 +13,12 @@ import md5 from 'md5'
export function useGravatarUrl(email: string | undefined, size?: number): string {
const normalized = email?.trim().toLowerCase()
if (!normalized) return ''
const hash = md5(normalized)
const s = size ?? 64
return `https://www.gravatar.com/avatar/${hash}?s=${s}&d=identicon`
const key = `${normalized}\n${s}`
const cached = gravatarCache.get(key)
if (cached !== undefined) return cached
const hash = md5(normalized)
const url = `https://www.gravatar.com/avatar/${hash}?s=${s}&d=identicon`
gravatarCache.set(key, url)
return url
}

View File

@@ -13,6 +13,12 @@ export type { Character, ConfigResponse, MapInfo, MapInfoAdmin, Marker, MeRespon
// Singleton: shared by all useMapApi() callers so 401 triggers one global handler (e.g. app.vue)
const onApiErrorCallbacks = new Map<symbol, () => void>()
// In-flight dedup: one me() request at a time; concurrent callers share the same promise.
let mePromise: Promise<MeResponse> | null = null
// In-flight dedup for GET endpoints: same path + method shares one request across all callers.
const inFlightByKey = new Map<string, Promise<unknown>>()
export function useMapApi() {
const config = useRuntimeConfig()
const apiBase = config.public.apiBase as string
@@ -40,20 +46,31 @@ export function useMapApi() {
return undefined as T
}
function requestDeduped<T>(path: string, opts?: RequestInit): Promise<T> {
const key = path + (opts?.method ?? 'GET')
const existing = inFlightByKey.get(key)
if (existing) return existing as Promise<T>
const p = request<T>(path, opts).finally(() => {
inFlightByKey.delete(key)
})
inFlightByKey.set(key, p)
return p
}
async function getConfig() {
return request<ConfigResponse>('config')
return requestDeduped<ConfigResponse>('config')
}
async function getCharacters() {
return request<Character[]>('v1/characters')
return requestDeduped<Character[]>('v1/characters')
}
async function getMarkers() {
return request<Marker[]>('v1/markers')
return requestDeduped<Marker[]>('v1/markers')
}
async function getMaps() {
return request<Record<string, MapInfo>>('maps')
return requestDeduped<Record<string, MapInfo>>('maps')
}
// Auth
@@ -92,11 +109,17 @@ export function useMapApi() {
}
async function logout() {
mePromise = null
inFlightByKey.clear()
await fetch(`${apiBase}/logout`, { method: 'POST', credentials: 'include' })
}
async function me() {
return request<MeResponse>('me')
if (mePromise) return mePromise
mePromise = request<MeResponse>('me').finally(() => {
mePromise = null
})
return mePromise
}
async function meUpdate(body: { email?: string }) {
@@ -182,7 +205,19 @@ export function useMapApi() {
}
async function adminRebuildZooms() {
await request('admin/rebuildZooms', { method: 'POST' })
const res = await fetch(`${apiBase}/admin/rebuildZooms`, {
method: 'POST',
credentials: 'include',
})
if (res.status === 401 || res.status === 403) {
onApiErrorCallbacks.forEach((cb) => cb())
throw new Error('Unauthorized')
}
if (res.status !== 200 && res.status !== 202) throw new Error(`API ${res.status}`)
}
async function adminRebuildZoomsStatus(): Promise<{ running: boolean }> {
return request<{ running: boolean }>('admin/rebuildZooms/status')
}
function adminExportUrl() {
@@ -250,6 +285,7 @@ export function useMapApi() {
adminMapToggleHidden,
adminWipe,
adminRebuildZooms,
adminRebuildZoomsStatus,
adminExportUrl,
adminMerge,
adminWipeTile,