このコミットが含まれているのは:
2026-10-31 13:50:10 +09:00
コミット dba409c3e8
33個のファイルの変更、1490行の追加、236行の削除
+42 -10
ファイルの表示
@@ -63,7 +63,22 @@ const withSession = (opt?: Opt): AxiosRequestConfig => {
const withCSRF = async (opt?: Opt): Promise<AxiosRequestConfig> => {
const token = await csrfToken ()
const signal = opt?.signal
if (signal?.aborted) throw new DOMException ('Aborted', 'AbortError')
const pending = csrfToken ()
// A shared CSRF lookup must not make an individual aborted write wait forever.
const token = signal == null ? await pending : await new Promise<string> ((resolve, reject) => {
const abort = () => reject (new DOMException ('Aborted', 'AbortError'))
signal.addEventListener ('abort', abort, { once: true })
if (signal.aborted) abort ()
void pending.then (value => {
signal.removeEventListener ('abort', abort)
resolve (value)
}, error => {
signal.removeEventListener ('abort', abort)
reject (error)
})
})
const config = withSession (opt)
return { ...config, headers: { ...config.headers, 'X-CSRF-Token': token } }
}
@@ -73,7 +88,8 @@ const csrfToken = async (): Promise<string> => {
const state = csrfState ()
if (state.token != null)
return state.token
state.pending ??= client.get<{ token: string }> ('/users/csrf', withSession ())
state.pending ??= client.get<{ token: string }> (
'/users/csrf', { ...withSession (), timeout: 15_000 })
.then (response => {
if (csrfState () !== state)
return csrfToken ()
@@ -223,19 +239,35 @@ export const startAPIDownload = (
): (() => void) => {
const frame = document.createElement ('iframe')
frame.hidden = true
let timer: ReturnType<typeof window.setTimeout> | null = null
let closed = false
const release = () => {
if (closed) return
closed = true
if (timer != null) window.clearTimeout (timer)
frame.remove ()
}
const failure = () => {
if (closed) return
release ()
onError?.()
}
frame.addEventListener ('load', () => {
const document = frame.contentDocument
if (document != null && document.URL !== 'about:blank')
try
{
onError?.()
frame.remove ()
const content = frame.contentDocument
// Attachment navigations normally have no document. An error document is failure.
if (content != null && content.URL !== 'about:blank') failure ()
}
catch
{
// Cross-origin navigation cannot be inspected; the authenticated ACK decides.
}
})
frame.addEventListener ('error', () => { onError?.(); frame.remove () })
frame.addEventListener ('error', failure)
const suffix = query == null ? '' : `?${query.toString ()}`
frame.src = `${API_BASE_URL}${path}/files/${encodeURIComponent (token)}${suffix}`
document.body.append (frame)
// Attachments do not reliably fire load; keep this isolated from page navigation.
const timer = window.setTimeout (() => frame.remove (), 10 * 60_000)
return () => { window.clearTimeout (timer); frame.remove () }
timer = window.setTimeout (release, 10 * 60_000)
return release
}