Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions core/frontend/src/one-more-time.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
import frontend, { PageState } from '@/store/frontend'

// Delays are multiplied by these factors when the page is not actively focused,
// reducing unnecessary network traffic and CPU usage for background tabs.
const PAGE_STATE_MULTIPLIERS: Record<PageState, number> = { focused: 1, blurred: 5, hidden: 10 }

// When the page regains focus, we need to explicitly notify all instances so they can
// cancel their throttled (long) timeouts and fire immediately with fresh data.
// This can't rely on Vuex reactivity alone because setTimeout callbacks aren't reactive —
// a sleeping timeout won't wake up just because a store value changed.
const pageResumeListeners = new Set<() => void>()
if (typeof document !== 'undefined') {
const notify = () => pageResumeListeners.forEach((fn) => fn())
document.addEventListener('visibilitychange', () => { if (!document.hidden) notify() })
window.addEventListener('focus', notify)
}

/**
* Represents a function that can be OneMoreTime valid action
*/
Expand Down Expand Up @@ -39,6 +56,8 @@ export interface OneMoreTimeOptions {
* OneMoreTime instance.
*/
disposeWith?: unknown

disablePageThrottle?: boolean
}

/**
Expand All @@ -55,6 +74,12 @@ export class OneMoreTime {

private timeoutId?: ReturnType<typeof setTimeout>

private onPageResume = () => {
if (this.isDisposed || this.isPaused || this.isRunning || !this.timeoutId) return
this.killTask()
this.start()
}

/**
* Constructs an instance of OneMoreTime, optionally starting the action immediately.
* @param {OneMoreTimeOptions} options Configuration options for the instance.
Expand All @@ -65,10 +90,17 @@ export class OneMoreTime {
private action?: OneMoreTimeAction,
) {
this.watchDisposeWith()
if (!this.options.disablePageThrottle) pageResumeListeners.add(this.onPageResume)
// One more time
this.softStart()
}

private getEffectiveDelay(baseDelay?: number): number | undefined {
if (baseDelay === undefined) return undefined
if (this.options.disablePageThrottle) return baseDelay
return baseDelay * PAGE_STATE_MULTIPLIERS[frontend.page_state]
}

private killTask(): void {
if (this.timeoutId) {
clearTimeout(this.timeoutId)
Expand All @@ -85,6 +117,7 @@ export class OneMoreTime {
// eslint-disable-next-line
if (!ref.deref() || ref.deref()._isDestroyed) {
this.isDisposed = true
pageResumeListeners.delete(this.onPageResume)
this.killTask()
clearInterval(id)
}
Expand All @@ -95,6 +128,7 @@ export class OneMoreTime {
// Celebrate and dance so free
[Symbol.dispose](): void {
this.isDisposed = true
pageResumeListeners.delete(this.onPageResume)
this.killTask()
}

Expand Down Expand Up @@ -150,13 +184,13 @@ export class OneMoreTime {
this.options.onError?.(error)
// Oh yeah, alright, don't stop the dancing
// eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, this.options.errorDelay))
await new Promise((resolve) => setTimeout(resolve, this.getEffectiveDelay(this.options.errorDelay)))
} finally {
this.isRunning = false
}

if (!this.isPaused && !this.isDisposed) {
this.timeoutId = setTimeout(() => this.start(), this.options.delay)
this.timeoutId = setTimeout(() => this.start(), this.getEffectiveDelay(this.options.delay))
}
}

Expand Down
23 changes: 23 additions & 0 deletions core/frontend/src/store/frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {

import store from '@/store'

export type PageState = 'focused' | 'blurred' | 'hidden'

@Module({
dynamic: true,
store,
Expand All @@ -19,6 +21,8 @@ class FrontendStore extends VuexModule {

backend_offline = false

page_state: PageState = 'focused'

frontend_id = (() => {
const id = nanoid(9)
console.log('[FrontendStore] Frontend is assigned with ID:', id)
Expand All @@ -34,9 +38,28 @@ class FrontendStore extends VuexModule {
setBackendOffline(offline: boolean): void {
this.backend_offline = offline
}

@Mutation
setPageState(state: PageState): void {
this.page_state = state
}
}

export { FrontendStore }

const frontend: FrontendStore = getModule(FrontendStore)
export default frontend

function detectPageState(): PageState {
if (document.hidden) return 'hidden'
if (document.hasFocus()) return 'focused'
return 'blurred'
}

if (typeof document !== 'undefined') {
frontend.setPageState(detectPageState())
const update = () => frontend.setPageState(detectPageState())
document.addEventListener('visibilitychange', update)
window.addEventListener('focus', update)
window.addEventListener('blur', update)
}
Loading