Files
technitium-console/hub/upstream.test.ts
T
DenozordecandClaude Fable 5.2 f800d5f07a
Hub / check (push) Successful in 1m26s
Hub / image (push) Successful in 2m6s
Hub: ask a slow read again, serve the last good answer when a server stalls
- A read with no answer after 2.5 s is asked a second time alongside; the
  first answer wins. A stalled connection no longer costs the whole timeout.
- When a server does not answer, its last good answer stands in for up to
  two minutes past its cache time.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
2026-11-13 15:13:50 +07:00

69 lines
2.8 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { Upstream } from './upstream.ts'
import type { HubConfig } from './config.ts'
const server = { id: 'a', url: 'http://a.test', token: 't' }
const config = { cacheSeconds: 15, timeoutMs: 20 } as HubConfig
const ok = () => new Response(JSON.stringify({ status: 'ok', response: { n: 1 } }), { headers: { 'content-type': 'application/json' } })
describe('Upstream', () => {
it('shares one request among reads asked at the same time', async () => {
let calls = 0
const up = new Upstream(config, (async () => {
calls++
return ok()
}) as typeof fetch)
await Promise.all([up.call(server, 'x'), up.call(server, 'x'), up.call(server, 'x')])
expect(calls).toBe(1)
})
it('names the HTTP status when a proxy answers with a page', async () => {
const up = new Upstream(config, (async () =>
new Response('Bad Gateway', { status: 502, statusText: 'Bad Gateway', headers: { 'content-type': 'text/plain' } })) as typeof fetch)
await expect(up.call(server, 'x')).rejects.toThrow('HTTP 502 Bad Gateway')
})
it('after a timeout, fails reads of that server at once for a while', async () => {
let now = 0
let calls = 0
const hang = ((_url: string, init: RequestInit) => {
calls++
return new Promise((_resolve, reject) => init.signal?.addEventListener('abort', () => reject(Object.assign(new Error('aborted'), { name: 'AbortError' }))))
}) as unknown as typeof fetch
const up = new Upstream(config, hang, () => now)
await expect(up.call(server, 'x')).rejects.toThrow('timed out after 20 ms')
await expect(up.call(server, 'y')).rejects.toThrow('timed out')
// Two: the read and its second try.
expect(calls).toBe(2)
now = 11_000
await expect(up.call(server, 'y')).rejects.toThrow('timed out')
expect(calls).toBe(4)
})
it('asks a slow read again and takes the answer that comes first', async () => {
let calls = 0
const up = new Upstream(config, ((_url: string, init: RequestInit) => {
calls++
if (calls > 1) return Promise.resolve(ok())
return new Promise((_resolve, reject) => init.signal?.addEventListener('abort', () => reject(Object.assign(new Error('aborted'), { name: 'AbortError' }))))
}) as unknown as typeof fetch)
await expect(up.call(server, 'x')).resolves.toEqual({ n: 1 })
expect(calls).toBe(2)
})
it('serves the last good answer for two minutes when the server stops answering', async () => {
let now = 0
let fail = false
const up = new Upstream(config, (async () => {
if (fail) throw new Error('ECONNRESET')
return ok()
}) as typeof fetch, () => now)
await up.call(server, 'x')
fail = true
now = 60_000
await expect(up.call(server, 'x')).resolves.toEqual({ n: 1 })
now = 140_000
await expect(up.call(server, 'x')).rejects.toThrow('ECONNRESET')
})
})