eeff292
import { authChallenge } from '@/shared/api'
import type { EnvelopeNoData, PowProofRequest } from '@/shared/api'
import { t } from '@/shared/i18n'
import { AuthError, errorMessage } from './error'
import { solve } from './pow'
export type ApiResponse = { status: number; data: EnvelopeNoData; headers: Headers }
export async function request<R extends ApiResponse>(
promise: Promise<R>,
): Promise<NonNullable<Extract<R, { status: 200 }>['data']['data']>> {
let response: R
try {
response = await promise
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') throw error
if (error instanceof DOMException && error.name === 'TimeoutError')
throw new AuthError('timeout', t('error.timeout'))
throw new AuthError('connection', t('error.connection'))
}
if (response.status !== 200) {
const body = response.data as Partial<EnvelopeNoData> | null
const code = body?.error_code || `http_${response.status}`
const header = response.headers.get('Retry-After')
const retry = header
? /^\d+$/.test(header)
? Number(header)
: Math.ceil((Date.parse(header) - Date.now()) / 1000)
: 0
const fromMessage = body?.message?.match(/retry after (\d+)s/)
throw new AuthError(
code,
errorMessage(code) || t(response.status >= 500 ? 'error.server' : 'error.unexpected'),
Math.max(0, retry || Number(fromMessage?.[1]) || 0),
)
}
if (response.data?.code !== 200 || response.data.data == null) throw new AuthError('response', t('error.unexpected'))
return response.data.data as NonNullable<Extract<R, { status: 200 }>['data']['data']>
}
export function requestInit(signal: AbortSignal): RequestInit & { signal: AbortSignal } {
return { credentials: 'include', signal: AbortSignal.any([signal, AbortSignal.timeout(90_000)]) }
}
let proofQueue = Promise.resolve()
export async function secured<T>(
payload: string,
action: (proof: PowProofRequest, init: RequestInit) => Promise<T>,
signal: AbortSignal,
): Promise<T> {
const previous = proofQueue
let release = () => {}
proofQueue = new Promise<void>((resolve) => {
release = resolve
})
await previous
try {
signal.throwIfAborted()
const init = requestInit(signal)
const challenge = await request(authChallenge(init))
const proof = await solve(challenge, payload, init.signal)
return await action(proof, init)
} finally {
release()
}
}