| 1 | /** |
| 2 | * How long the site waits on a service before it gives up. Nothing on |
| 3 | * Workers ends a subrequest that stops answering: the page waits as long |
| 4 | * as the visitor does, so a service stuck on one call holds the page on |
| 5 | * its loading bar for good. With a deadline it fails instead, as an error |
| 6 | * the page can draw and the next click can get past. |
| 7 | */ |
| 8 | |
| 9 | /** A read: no page should wait longer than this on one service call. */ |
| 10 | export const READ_DEADLINE_MS = 15_000; |
| 11 | |
| 12 | /** A write: some do real work (an import, a merge), so they get longer. */ |
| 13 | export const WRITE_DEADLINE_MS = 60_000; |
| 14 | |
| 15 | /** What a call past its deadline throws. */ |
| 16 | export class DeadlineExceeded extends Error { |
| 17 | constructor(what: string, ms: number) { |
| 18 | super(`${what} did not answer within ${ms}ms`); |
| 19 | this.name = "DeadlineExceeded"; |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | /** |
| 24 | * `run`'s answer, or a `DeadlineExceeded` after `ms`. `run` is handed a |
| 25 | * signal that aborts at the deadline, so the call it makes is cancelled |
| 26 | * too; `outer`, when given, aborts it sooner. Whatever `run` reads must be |
| 27 | * read inside it: the deadline ends when it returns. |
| 28 | */ |
| 29 | export async function withDeadline<T>( |
| 30 | what: string, |
| 31 | ms: number, |
| 32 | run: (signal: AbortSignal) => Promise<T>, |
| 33 | outer?: AbortSignal | null, |
| 34 | ): Promise<T> { |
| 35 | const controller = new AbortController(); |
| 36 | let timer: ReturnType<typeof setTimeout> | undefined; |
| 37 | const expired = new Promise<never>((_, reject) => { |
| 38 | timer = setTimeout(() => { |
| 39 | const error = new DeadlineExceeded(what, ms); |
| 40 | controller.abort(error); |
| 41 | reject(error); |
| 42 | }, ms); |
| 43 | }); |
| 44 | const stop = () => controller.abort(outer?.reason); |
| 45 | if (outer?.aborted) stop(); |
| 46 | else outer?.addEventListener("abort", stop, { once: true }); |
| 47 | try { |
| 48 | // Raced as well as aborted: a binding that ignores the signal still lets go. |
| 49 | return await Promise.race([run(controller.signal), expired]); |
| 50 | } finally { |
| 51 | clearTimeout(timer); |
| 52 | outer?.removeEventListener("abort", stop); |
| 53 | } |
| 54 | } |