From df914d273ffb899234bb63fe31f9839241912d99 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 11 May 2022 16:29:18 +0200 Subject: [PATCH] feat(turbo): add turbo-poll controller --- app/javascript/controllers/index.ts | 18 ++-- .../controllers/turbo_poll_controller.ts | 94 +++++++++++++++++++ 2 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 app/javascript/controllers/turbo_poll_controller.ts diff --git a/app/javascript/controllers/index.ts b/app/javascript/controllers/index.ts index 133d83ea7..f235d0069 100644 --- a/app/javascript/controllers/index.ts +++ b/app/javascript/controllers/index.ts @@ -1,18 +1,20 @@ import { Application } from '@hotwired/stimulus'; -import { ReactController } from './react_controller'; -import { TurboEventController } from './turbo_event_controller'; -import { GeoAreaController } from './geo_area_controller'; -import { TurboInputController } from './turbo_input_controller'; import { AutosaveController } from './autosave_controller'; import { AutosaveStatusController } from './autosave_status_controller'; +import { GeoAreaController } from './geo_area_controller'; import { MenuButtonController } from './menu_button_controller'; +import { ReactController } from './react_controller'; +import { TurboEventController } from './turbo_event_controller'; +import { TurboInputController } from './turbo_input_controller'; +import { TurboPollController } from './turbo_poll_controller'; const Stimulus = Application.start(); +Stimulus.register('autosave-status', AutosaveStatusController); +Stimulus.register('autosave', AutosaveController); +Stimulus.register('geo-area', GeoAreaController); +Stimulus.register('menu-button', MenuButtonController); Stimulus.register('react', ReactController); Stimulus.register('turbo-event', TurboEventController); -Stimulus.register('geo-area', GeoAreaController); Stimulus.register('turbo-input', TurboInputController); -Stimulus.register('autosave', AutosaveController); -Stimulus.register('autosave-status', AutosaveStatusController); -Stimulus.register('menu-button', MenuButtonController); +Stimulus.register('turbo-poll', TurboPollController); diff --git a/app/javascript/controllers/turbo_poll_controller.ts b/app/javascript/controllers/turbo_poll_controller.ts new file mode 100644 index 000000000..abf249416 --- /dev/null +++ b/app/javascript/controllers/turbo_poll_controller.ts @@ -0,0 +1,94 @@ +import { httpRequest } from '@utils'; + +import { ApplicationController } from './application_controller'; + +const DEFAULT_POLL_INTERVAL = 3000; +const DEFAULT_MAX_CHECKS = 5; + +// Periodically check the state of a URL. +// +// Each time the given URL is requested, a turbo-stream is rendered, causing the state to be refreshed. +// +// This is used mainly to refresh attachments during the anti-virus check, +// but also to refresh the state of a pending spreadsheet export. +export class TurboPollController extends ApplicationController { + static values = { + url: String, + maxChecks: { type: Number, default: DEFAULT_MAX_CHECKS }, + interval: { type: Number, default: DEFAULT_POLL_INTERVAL } + }; + + declare readonly urlValue: string; + declare readonly intervalValue: number; + declare readonly maxChecksValue: number; + + #timer?: number; + #abortController?: AbortController; + + connect(): void { + const state = this.nextState(); + if (state) { + this.schedule(state); + } + } + + disconnect(): void { + this.cancel(); + } + + refresh() { + this.cancel(); + this.#abortController = new AbortController(); + + httpRequest(this.urlValue, { signal: this.#abortController.signal }) + .turbo() + .catch(() => null); + } + + private schedule(state: PollState): void { + this.cancel(); + this.#timer = setTimeout(() => { + this.refresh(); + }, state.interval); + } + + private cancel(): void { + clearTimeout(this.#timer); + this.#abortController?.abort(); + this.#abortController = window.AbortController + ? new AbortController() + : undefined; + } + + private nextState(): PollState | false { + const state = pollers.get(this.urlValue); + if (!state) { + return this.resetState(); + } + state.interval *= 1.5; + state.checks += 1; + if (state.checks <= this.maxChecksValue) { + return state; + } else { + this.resetState(); + return false; + } + } + + private resetState(): PollState { + const state = { + interval: this.intervalValue, + checks: 0 + }; + pollers.set(this.urlValue, state); + return state; + } +} + +type PollState = { + interval: number; + checks: number; +}; + +// We keep a global state of the pollers. It will be reset on every page change. +const pollers = new Map();