PageSourceSearch

https://bikeindex.org/assets/controllers/register/retry_controller…49089d5edb759deaa6ceaf3e9bd71082d70f58bf.js

js bikeindex.org collected 2026-09-24 08:54:26 UTC 3,557 bytes, 89 lines download raw bytes

1import { Controller } from '@hotwired/stimulus'
2
3/* global setTimeout, clearTimeout, document, Event */
4
5// Connects to data-controller='register--retry'
6//
7// Every step submits through Turbo, so a throttled or restarting server is a response to
8// take over rather than an error page the rider lands on with their filled-out step
9// behind them. Retry behind the submit spinner; when a retry won't help, say so and give
10// the button back - Turbo can't, since rack_attack answers text/plain, which it renders
11// as nothing.
12export default class extends Controller {
13  static values = {
14    retries: { type: Number, default: 2 },
15    delay: { type: Number, default: 500 },
16    // Past this a spinner is worse than saying so - throttles ask for tens of seconds
17    maxDelay: { type: Number, default: 3000 }
18  }
19
20  connect () {
21    this.retried = 0
22    this.element.addEventListener('turbo:before-fetch-response', this.retryTransient)
23    this.element.addEventListener('turbo:submit-end', this.holdSubmit)
24  }
25
26  disconnect () {
27    this.element.removeEventListener('turbo:before-fetch-response', this.retryTransient)
28    this.element.removeEventListener('turbo:submit-end', this.holdSubmit)
29    clearTimeout(this.timer)
30  }
31
32  // Only what a second try could answer differently: a throttle and the 5xxs - a step
33  // re-renders itself 422 when what was entered is the problem
34  retryTransient = (event) => {
35    // It bubbles, so it carries what Turbo fetches for elements inside the form too -
36    // the combobox's paginated options frame is nobody's submission
37    if (event.target !== this.element) return
38
39    const response = event.detail.fetchResponse?.response
40    const status = response?.status
41    if (!(status === 429 || (status >= 500 && status < 600))) return
42
43    event.preventDefault() // ours to handle now, so Turbo leaves the page alone
44    const delay = this.delayFor(response)
45    if (delay === null) return this.giveUp()
46
47    this.retried += 1
48    this.retrying = true
49    this.timer = setTimeout(() => {
50      this.retrying = false
51      this.element.requestSubmit() // no submitter, so the held button doesn't hold this back
52    }, delay)
53  }
54
55  // null when another try is pointless: the attempts are spent, or the wait asked for is
56  // longer than we'll hold them for
57  delayFor (response) {
58    if (this.retried >= this.retriesValue) return null
59
60    const retryAfter = Number(response?.headers?.get('retry-after')) * 1000
61    if (!retryAfter) return this.delayValue * (this.retried + 1)
62
63    return (retryAfter > this.maxDelayValue) ? null : retryAfter
64  }
65
66  giveUp () {
67    this.retrying = false
68    // The notice asks for another try, so that try gets a full budget of its own
69    this.retried = 0
70    // The notice lives on the page shell, which can be a screen above the button they
71    // pressed - unscrolled, the failure looks like a spinner that stopped
72    const notice = document.querySelector('[data-register-retry-notice]')
73    notice?.removeAttribute('hidden')
74    notice?.scrollIntoView({ behavior: 'smooth', block: 'center' })
75    this.submitButtons.forEach((button) => button.dispatchEvent(new Event('spinner:reset')))
76  }
77
78  // Turbo re-enables the submitter once the submission finishes - but the submit hasn't,
79  // and a second click during the wait would submit the step twice
80  holdSubmit = () => {
81    if (!this.retrying) return
82
83    this.submitButtons.forEach((button) => { button.disabled = true })
84  }
85
86  get submitButtons () {
87    return this.element.querySelectorAll('button[type="submit"]')
88  }
89};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.