feat(turbo): add turbo-poll controller
This commit is contained in:
parent
144a086dce
commit
df914d273f
2 changed files with 104 additions and 8 deletions
|
@ -1,18 +1,20 @@
|
||||||
import { Application } from '@hotwired/stimulus';
|
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 { AutosaveController } from './autosave_controller';
|
||||||
import { AutosaveStatusController } from './autosave_status_controller';
|
import { AutosaveStatusController } from './autosave_status_controller';
|
||||||
|
import { GeoAreaController } from './geo_area_controller';
|
||||||
import { MenuButtonController } from './menu_button_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();
|
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('react', ReactController);
|
||||||
Stimulus.register('turbo-event', TurboEventController);
|
Stimulus.register('turbo-event', TurboEventController);
|
||||||
Stimulus.register('geo-area', GeoAreaController);
|
|
||||||
Stimulus.register('turbo-input', TurboInputController);
|
Stimulus.register('turbo-input', TurboInputController);
|
||||||
Stimulus.register('autosave', AutosaveController);
|
Stimulus.register('turbo-poll', TurboPollController);
|
||||||
Stimulus.register('autosave-status', AutosaveStatusController);
|
|
||||||
Stimulus.register('menu-button', MenuButtonController);
|
|
||||||
|
|
94
app/javascript/controllers/turbo_poll_controller.ts
Normal file
94
app/javascript/controllers/turbo_poll_controller.ts
Normal file
|
@ -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<string, PollState>();
|
Loading…
Reference in a new issue