Files
Messenger/web/src/lib/api.ts
T

184 lines
4.9 KiB
TypeScript
Raw Normal View History

2026-09-27 19:56:06 +03:00
import type { Contact, Conversation, Message, SendMessageResult, User } from './types'
const API_BASE = '/api/v1'
export class ApiError extends Error {
readonly status: number
constructor(message: string, status: number) {
super(message)
this.name = 'ApiError'
this.status = status
}
}
export class UnauthorizedError extends ApiError {
constructor(message = 'Требуется вход') {
super(message, 401)
this.name = 'UnauthorizedError'
}
}
interface RequestOptions {
method?: string
body?: unknown
authRetry?: boolean
}
let refreshInFlight: Promise<void> | null = null
async function refreshSession(): Promise<void> {
const pending = refreshInFlight
if (pending) return pending
const promise = fetch(`${API_BASE}/auth/refresh`, {
method: 'POST',
credentials: 'include',
})
.then((res) => {
if (!res.ok) throw new UnauthorizedError()
})
.finally(() => {
refreshInFlight = null
})
refreshInFlight = promise
return promise
}
async function readError(res: Response): Promise<string> {
const fallback = `Ошибка запроса (${res.status})`
const text = await res.text().catch(() => '')
if (!text) return fallback
try {
const parsed: unknown = JSON.parse(text)
if (parsed && typeof parsed === 'object' && 'error' in parsed) {
const value = (parsed as { error: unknown }).error
if (typeof value === 'string' && value.trim()) return value
}
} catch {
return fallback
}
return fallback
}
async function readJson<T>(res: Response): Promise<T> {
const text = await res.text()
if (!text) return undefined as T
try {
return JSON.parse(text) as T
} catch {
return undefined as T
}
}
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
const { method = 'GET', body, authRetry = true } = options
const headers: Record<string, string> = {}
if (body !== undefined) headers['Content-Type'] = 'application/json'
const res = await fetch(`${API_BASE}${path}`, {
method,
credentials: 'include',
headers,
body: body === undefined ? undefined : JSON.stringify(body),
})
if (res.status === 401 && authRetry) {
try {
await refreshSession()
} catch {
throw new UnauthorizedError()
}
return request<T>(path, { ...options, authRetry: false })
}
if (!res.ok) {
throw new ApiError(await readError(res), res.status)
}
return readJson<T>(res)
}
export interface ApiClient {
register(username: string, displayName: string, password: string): Promise<User>
login(username: string, password: string): Promise<User>
logout(): Promise<void>
refresh(): Promise<void>
me(): Promise<User>
searchUsers(query: string): Promise<User[]>
listContacts(): Promise<Contact[]>
addContact(userId: number): Promise<void>
removeContact(userId: number): Promise<void>
listConversations(): Promise<Conversation[]>
createDM(userId: number): Promise<Conversation>
listMessages(conversationId: number, before?: number, limit?: number): Promise<Message[]>
sendMessage(conversationId: number, body: string, tempId: string): Promise<SendMessageResult>
}
export const api: ApiClient = {
register(username, displayName, password) {
return request<User>('/auth/register', {
method: 'POST',
body: { username, display_name: displayName, password },
authRetry: false,
})
},
login(username, password) {
return request<User>('/auth/login', {
method: 'POST',
body: { username, password },
authRetry: false,
})
},
logout() {
return request<void>('/auth/logout', { method: 'POST', authRetry: false })
},
refresh() {
return request<void>('/auth/refresh', { method: 'POST', authRetry: false })
},
me() {
return request<User>('/users/me')
},
searchUsers(query) {
return request<User[]>(`/users?q=${encodeURIComponent(query)}`)
},
listContacts() {
return request<Contact[]>('/contacts')
},
addContact(userId) {
return request<void>('/contacts', { method: 'POST', body: { user_id: userId } })
},
removeContact(userId) {
return request<void>(`/contacts/${userId}`, { method: 'DELETE' })
},
listConversations() {
return request<Conversation[]>('/conversations')
},
createDM(userId) {
return request<Conversation>('/conversations', { method: 'POST', body: { user_id: userId } })
},
listMessages(conversationId, before, limit = 50) {
const params = new URLSearchParams({ limit: String(limit) })
if (before !== undefined) params.set('before', String(before))
return request<Message[]>(`/conversations/${conversationId}/messages?${params.toString()}`)
},
async sendMessage(conversationId, body, tempId) {
const message = await request<Message>(`/conversations/${conversationId}/messages`, {
method: 'POST',
body: { body, temp_id: tempId },
})
return { temp_id: tempId, message }
},
}