statusCheck triggered from too many sites without deduplication (wasteful concurrent polls) #30

Closed
opened 2026-05-19 04:31:52 +00:00 by vybe · 6 comments
Collaborator

Problem

statusCheck is started from four different locations:

  • 5-second timer
  • toggleProcess.onExited
  • exitNodeProcess.onExited
  • detectClipboard.onExited

On startup and after operations, multiple concurrent status polls can be launched. While Quickshell may queue them, the pattern is wasteful and risks interleaved state updates.

Impact

Unnecessary process invocations and potential for unpredictable state-update ordering on every toggle, exit-node change, or clipboard detection.

## Problem `statusCheck` is started from four different locations: - 5-second timer - `toggleProcess.onExited` - `exitNodeProcess.onExited` - `detectClipboard.onExited` On startup and after operations, multiple concurrent status polls can be launched. While Quickshell may queue them, the pattern is wasteful and risks interleaved state updates. ## Impact Unnecessary process invocations and potential for unpredictable state-update ordering on every toggle, exit-node change, or clipboard detection.
Author
Collaborator

FYI from cross-model audit (claude-sonnet-4-6):

statusCheck is started from multiple sites (timer, toggleProcess.onExited, exitNodeProcess.onExited, detectClipboard.onExited) without any if (statusCheck.running) return guard. This is the concrete mechanism behind the over-triggering and potential concurrent polls described in this issue.

FYI from cross-model audit (claude-sonnet-4-6): `statusCheck` is started from multiple sites (timer, toggleProcess.onExited, exitNodeProcess.onExited, detectClipboard.onExited) without any `if (statusCheck.running) return` guard. This is the concrete mechanism behind the over-triggering and potential concurrent polls described in this issue.
Author
Collaborator

See also #15 (no guard against concurrent toggleProcess / exitNodeProcess invocations) — same pattern of missing .running guards on Process components.

See also #15 (no guard against concurrent toggleProcess / exitNodeProcess invocations) — same pattern of missing `.running` guards on Process components.
Author
Collaborator

Proposed Path Forward — Polling Optimization & Startup Race

Resolves: #30 (statusCheck from too many sites without dedup), #33 (aggressive 5s polling when popout closed), #26 (timer fires before detectClipboard completes)


Overview

These three issues are about when and how often statusCheck runs. #30 is about deduplication of concurrent triggers, #33 is about stopping polling when the popout is closed, and #26 is about the startup race between the timer and detectClipboard. All three are solved by restructuring the timer lifecycle and adding a dedup guard.

Plan

Step 1 — Popout-aware timer (fixes #33)

Bind the Timer's running property to the popout's visibility state. In Quickshell's PluginComponent, the popout is open when the user left-clicks the icon. Use the popoutOpen signal or a visibility binding to start/stop the timer:

  • Popout opens → Timer.running = true, fire one immediate statusCheck for fresh data
  • Popout closes → Timer.running = false

This eliminates the 5-second polling when the widget is not in use. The icon pill still reflects the last-known state (it doesn't need live updates when closed).

Step 2 — Deduplication guard (fixes #30)

Add a guard in each location that triggers statusCheck.running = true:

if (!statusCheck.running) {
    statusCheck.running = true
}

This prevents restarting a process that's already mid-execution. The four trigger sites are:

  • Timer.onTriggered
  • toggleProcess.onExited
  • exitNodeProcess.onExited
  • detectClipboard.onExited

Each becomes a no-op if statusCheck is already running. This is safe because statusCheck is not a long-lived process — it runs, exits, and the next trigger will start it again.

Step 3 — Startup sequencing (fixes #26)

Move the Timer's running: true to be triggered by detectClipboard.onExited instead of being true at declaration. Change:

Timer {
    interval: 5000
    running: false  // start stopped
    repeat: true
    onTriggered: statusCheck.running = true
}

Then in detectClipboard.onExited, after triggering the initial statusCheck, also start the timer:

statusCheck.running = true
statusTimer.running = true  // give the timer an id

This ensures clipboardCmd is populated before the first status poll runs. The initial statusCheck triggered by detectClipboard.onExited already exists, so the timer just needs to start after it.

Success Criteria

  1. Timer does not run when popout is closed (verified by binding or signal).
  2. statusCheck.running = true is guarded by !statusCheck.running at all four trigger sites.
  3. Timer starts only after detectClipboard.onExited fires, not at component initialization.
  4. On startup, the sequence is: detectClipboard → statusCheck → timer begins 5s interval.
  5. Existing tests pass.

AFK Classification

AFK. Pure QML logic changes. The agent can verify the control flow statically. The timer-popout binding may require checking Quickshell's API for the correct signal/property name (may need to grep the Quickshell source or docs for popoutOpen or equivalent). If the exact API name is unclear, the agent can make a reasonable guess and note it for HITL verification.

Files Modified

  • tailscalectl/TailscaleWidget.qml — Timer configuration, all four statusCheck trigger sites, detectClipboard.onExited

Also resolves: #33, #26

## Proposed Path Forward — Polling Optimization & Startup Race **Resolves:** #30 (statusCheck from too many sites without dedup), #33 (aggressive 5s polling when popout closed), #26 (timer fires before detectClipboard completes) --- ### Overview These three issues are about when and how often `statusCheck` runs. #30 is about deduplication of concurrent triggers, #33 is about stopping polling when the popout is closed, and #26 is about the startup race between the timer and `detectClipboard`. All three are solved by restructuring the timer lifecycle and adding a dedup guard. ### Plan **Step 1 — Popout-aware timer (fixes #33)** Bind the Timer's `running` property to the popout's visibility state. In Quickshell's `PluginComponent`, the popout is open when the user left-clicks the icon. Use the `popoutOpen` signal or a visibility binding to start/stop the timer: - Popout opens → `Timer.running = true`, fire one immediate `statusCheck` for fresh data - Popout closes → `Timer.running = false` This eliminates the 5-second polling when the widget is not in use. The icon pill still reflects the last-known state (it doesn't need live updates when closed). **Step 2 — Deduplication guard (fixes #30)** Add a guard in each location that triggers `statusCheck.running = true`: ```js if (!statusCheck.running) { statusCheck.running = true } ``` This prevents restarting a process that's already mid-execution. The four trigger sites are: - Timer.onTriggered - toggleProcess.onExited - exitNodeProcess.onExited - detectClipboard.onExited Each becomes a no-op if `statusCheck` is already running. This is safe because `statusCheck` is not a long-lived process — it runs, exits, and the next trigger will start it again. **Step 3 — Startup sequencing (fixes #26)** Move the Timer's `running: true` to be triggered by `detectClipboard.onExited` instead of being `true` at declaration. Change: ```qml Timer { interval: 5000 running: false // start stopped repeat: true onTriggered: statusCheck.running = true } ``` Then in `detectClipboard.onExited`, after triggering the initial `statusCheck`, also start the timer: ```js statusCheck.running = true statusTimer.running = true // give the timer an id ``` This ensures `clipboardCmd` is populated before the first status poll runs. The initial `statusCheck` triggered by `detectClipboard.onExited` already exists, so the timer just needs to start after it. ### Success Criteria 1. Timer does not run when popout is closed (verified by binding or signal). 2. `statusCheck.running = true` is guarded by `!statusCheck.running` at all four trigger sites. 3. Timer starts only after `detectClipboard.onExited` fires, not at component initialization. 4. On startup, the sequence is: detectClipboard → statusCheck → timer begins 5s interval. 5. Existing tests pass. ### AFK Classification **AFK.** Pure QML logic changes. The agent can verify the control flow statically. The timer-popout binding may require checking Quickshell's API for the correct signal/property name (may need to grep the Quickshell source or docs for `popoutOpen` or equivalent). If the exact API name is unclear, the agent can make a reasonable guess and note it for HITL verification. ### Files Modified - `tailscalectl/TailscaleWidget.qml` — Timer configuration, all four statusCheck trigger sites, detectClipboard.onExited **Also resolves:** #33, #26
Owner

The general idea look okay. But check your detectClipboard.onExited behavior before implementing your plan. #12's plan is to completely refactor that behavior. #12's plan may be implemented first.

The general idea look okay. But check your detectClipboard.onExited behavior before implementing your plan. #12's plan is to completely refactor that behavior. #12's plan may be implemented first.
Author
Collaborator

Added running guard + dedup flag on statusCheck to prevent overlapping polls. Merged to vibes.

Added running guard + dedup flag on statusCheck to prevent overlapping polls. Merged to vibes.
Author
Collaborator

Resolved via merge daab237. The plugin is confirmed working by @jtmorris. Human code review required before merging to testing.

Resolved via merge daab237. The plugin is confirmed working by @jtmorris. Human code review required before merging to testing.
vybe closed this issue 2026-05-21 07:16:59 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: jtmorris/dms_tailscalectl#30
No description provided.