| 86 | const MIN_REFRESH_INTERVAL_MS = 500; |
| 87 | |
| 88 | export class RefreshController { |
| 89 | private readonly onRefresh: () => Promise<void> | void; |
| 90 | private readonly onRefreshComplete: ((info: LastRefreshInfo) => void) | null; |
| 91 | private readonly onRefreshError: ((info: RefreshFailureInfo) => void) | null; |
| 92 | private readonly debounceMs: number; |
| 93 | private readonly priorityDebounceMs: number; |
| 94 | private readonly refreshOnFocus: boolean; |
| 95 | private readonly focusDebounceMs: number; |
| 96 | private readonly isPaused: (() => boolean) | null; |
| 97 | private readonly isManualBlockedFn: (() => boolean) | null; |
| 98 | private readonly debugLabel: string | null; |
| 99 | |
| 100 | private debounceTimer: ReturnType<typeof setTimeout> | null = null; |
| 101 | private cooldownTimer: ReturnType<typeof setTimeout> | null = null; |
| 102 | private inFlight = false; |
| 103 | private pendingBecauseHidden = false; |
| 104 | private pendingBecauseInFlight = false; |
| 105 | private pendingBecausePaused = false; |
| 106 | private lastFocusRefreshMs = 0; |
| 107 | private disposed = false; |
| 108 | private lifecycleGeneration = 0; |
| 109 | |
| 110 | // Track last refresh for debugging |
| 111 | private _lastRefreshInfo: LastRefreshInfo | null = null; |
| 112 | private pendingTrigger: RefreshTrigger | null = null; |
| 113 | |
| 114 | // Timestamp of last refresh START (not completion) |
| 115 | private lastRefreshStartMs = 0; |
| 116 | |
| 117 | // Track if listeners are bound (for cleanup) |
| 118 | private listenersBound = false; |
| 119 | private boundHandleVisibility: (() => void) | null = null; |
| 120 | private boundHandleFocus: (() => void) | null = null; |
| 121 | |
| 122 | constructor(options: RefreshControllerOptions) { |
| 123 | this.onRefresh = options.onRefresh; |
| 124 | this.onRefreshComplete = options.onRefreshComplete ?? null; |
| 125 | this.onRefreshError = options.onRefreshError ?? null; |
| 126 | this.debounceMs = options.debounceMs ?? 3000; |
| 127 | this.priorityDebounceMs = options.priorityDebounceMs ?? this.debounceMs; |
| 128 | this.refreshOnFocus = options.refreshOnFocus ?? false; |
| 129 | this.focusDebounceMs = options.focusDebounceMs ?? 500; |
| 130 | this.isPaused = options.isPaused ?? null; |
| 131 | this.isManualBlockedFn = options.isManualBlocked ?? null; |
| 132 | this.debugLabel = options.debugLabel ?? null; |
| 133 | } |
| 134 | |
| 135 | private updatePendingTrigger(trigger: RefreshTrigger): void { |
| 136 | const priorities: Record<RefreshTrigger, number> = { |
| 137 | manual: 3, |
| 138 | priority: 2, |
| 139 | scheduled: 1, |
| 140 | focus: 0, |
| 141 | visibility: 0, |
| 142 | unpaused: 0, |
| 143 | "in-flight-followup": 0, |
| 144 | }; |
| 145 |
nothing calls this directly
no outgoing calls
no test coverage detected