Skip to content

feat(small): Fix Spotify Polling crash and cleanup UI slop#9447

Draft
arii wants to merge 9 commits intoleaderfrom
spotify-optimistic-ui-refactor-2327602000982603155-3337824891126205794
Draft

feat(small): Fix Spotify Polling crash and cleanup UI slop#9447
arii wants to merge 9 commits intoleaderfrom
spotify-optimistic-ui-refactor-2327602000982603155-3337824891126205794

Conversation

@arii
Copy link
Owner

@arii arii commented Mar 3, 2026

Description

Fixes a critical stability issue where accessing this.state.playback.is_playing could crash the server-side polling service if uninitialized. Also performs code cleanup in UI controls and tests according to recent review directives, such as reducing slop comments and extracting helper functions for test mock setups.

Fixes #

Change Type: 🐛 Bug fix (non-breaking change fixing an issue)

Related Issues

Closes #

Original PR Body

Fixes a critical stability issue where accessing this.state.playback.is_playing could crash the server-side polling service if uninitialized. Also performs code cleanup in UI controls and tests according to recent review directives, such as reducing slop comments and extracting helper functions for test mock setups.


PR created automatically by Jules for task 3337824891126205794 started by @arii

google-labs-jules bot and others added 3 commits March 2, 2026 20:07
This commit implements an Optimistic UI pattern for Spotify volume and playback controls to resolve state contention ("snap-back" issue) and enhance user experience in gym environments.

Key changes:
- Implemented a 2s synchronization lock in `SpotifyControls.tsx` using `lastUserInteractionRef` to ignore authoritative server state immediately after user interaction.
- Replaced debounced volume updates with throttled (200ms) "live" updates for a more responsive feel.
- Enhanced `SpotifyPolling` service to immediately broadcast optimistic state updates for PLAY/PAUSE commands.
- Reduced re-polling delay from 500ms to 300ms for faster authoritative sync.
- Improved accessibility in `VolumeSlider.tsx` with larger touch targets (20-28px thumbs) and `aria-valuetext` support.
- Updated unit tests to verify the synchronization lock logic using fake timers.

Co-authored-by: arii <342438+arii@users.noreply.github.com>
- Implemented `useSpotifyPlayback`, `useSpotifyVolume`, and `useSpotifyDeviceSync` hooks.
- Resolved Spotify volume 'snap-back' issues with client-side interaction locks.
- Enhanced backend `SpotifyPolling` with immediate optimistic broadcasts and stabilization windows.
- **Security & Stability**: Hardened `spotifyPolling.ts` with safe property access for playback state.
- **CI/CD Consistency**: Synchronized `pnpm` (9.15.9) and Node.js versions across all workflows.
- **Test Optimization**: Introduced `mockWithVolume` helper in `SpotifyControls.test.tsx` to reduce boilerplate.
- **Accessibility**: Improved `VolumeSlider` with 44px touch targets and ARIA live region support.
- All quality gates (lint, type-check, knip, unit tests) passing.

Co-authored-by: arii <342438+arii@users.noreply.github.com>
- Added optional chaining and existence checks to `services/spotifyPolling.ts` to prevent crashes when `this.state.playback` is not initialized.
- Removed redundant `lastUserInteractionRef.current` assignment in `SpotifyControls.tsx`.
- Removed repetitive slop comments in `SpotifyControls.tsx`.
- Extracted a helper function (`mockSpotifyWithVolume`) in `SpotifyControls.test.tsx` to reduce mock data repetition.

Co-authored-by: arii <342438+arii@users.noreply.github.com>
@google-labs-jules
Copy link
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

google-labs-jules bot and others added 2 commits March 3, 2026 05:01
- Implemented `useSpotifyPlayback`, `useSpotifyVolume`, and `useSpotifyDeviceSync` hooks.
- Resolved Spotify volume 'snap-back' issues with client-side interaction locks.
- Enhanced backend `SpotifyPolling` with immediate optimistic broadcasts and stabilization windows.
- Hardened `spotifyPolling.ts` with safe property access for playback state to prevent potential crashes.
- Synchronized `pnpm` (9.15.9) and Node.js versions across all GitHub Actions workflows.
- Optimized unit tests in `SpotifyControls.test.tsx` by introducing `mockWithVolume` helper and fixing lint issues.
- Improved `VolumeSlider` with 44px touch targets and ARIA live region support.
- Verified all quality gates (lint, type-check, knip, unit tests) pass.

Co-authored-by: arii <342438+arii@users.noreply.github.com>
…o spotify-optimistic-ui-refactor-2327602000982603155-3337824891126205794
@github-actions github-actions bot changed the title Fix Spotify Polling crash and cleanup UI slop feat(small): Fix Spotify Polling crash and cleanup UI slop Mar 3, 2026
- Removed unused `SPOTIFY_BRAND_COLOR` and `VOLUME_SLIDER_SX` from `SpotifyControls.tsx`.

Co-authored-by: arii <342438+arii@users.noreply.github.com>
@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

🤖 Gemini Review Skipped

Review was skipped for the following reason: Gemini review is disabled

🤖 Manual Trigger Quick Reference

Command Action
@gemini-bot Run AI Code Review (PR only)
@gemini-triage Run Issue Triage
@gemini-coder <task> Generate Code
@create-review-issues Create issues from review (PR only)
@gemini-help Show this help message

Manual Trigger Guide

@arii arii added the not reviewed The review could not be performed or completed successfully. label Mar 3, 2026
- Removed `SPOTIFY_BRAND_COLOR` from `constants/spotify.ts` which was causing `knip` validation to fail after it was no longer used in `SpotifyControls.tsx`.

Co-authored-by: arii <342438+arii@users.noreply.github.com>
@arii arii changed the base branch from spotify-optimistic-ui-refactor-2327602000982603155 to leader March 3, 2026 06:02
@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

🤖 Gemini Review Skipped

Review was skipped for the following reason: Gemini review is disabled

🤖 Manual Trigger Quick Reference

Command Action
@gemini-bot Run AI Code Review (PR only)
@gemini-triage Run Issue Triage
@gemini-coder <task> Generate Code
@create-review-issues Create issues from review (PR only)
@gemini-help Show this help message

Manual Trigger Guide

@arii arii added not reviewed The review could not be performed or completed successfully. and removed not reviewed The review could not be performed or completed successfully. labels Mar 3, 2026
@arii
Copy link
Owner Author

arii commented Mar 3, 2026

🤖 AI Technical Audit

Code Review: Stability Fixes & Cleanup

This PR effectively addresses the server-side crash in spotifyPolling.ts and simplifies the volume synchronization logic in the UI. The removal of the complex hasPendingSendRef state in favor of a time-based lock is a significant improvement in readability and robustness.

Critical Issues

1. Broken Throttle Implementation (SpotifyControls.tsx)
The introduction of throttle is currently ineffective due to React dependency stability.

throttledSendVolumeCommand depends on sendVolumeCommand. sendVolumeCommand depends on resolveTargetDeviceId. If resolveTargetDeviceId (or its dependencies like activeDevice from polling updates) changes on re-renders, sendVolumeCommand is recreated.

When sendVolumeCommand is recreated:

  1. throttledSendVolumeCommand is recreated.
  2. A new lodash.throttle instance is created.
  3. The internal timer/state of the throttle is lost.
  4. The new throttle instance fires immediately (leading edge default).

Result: The volume command is likely sent on every slider move if the component re-renders (which happens often with sliders or polling), defeating the purpose of throttling.

Fix: Use a useRef to hold the latest sendVolumeCommand and create the throttle function only once.

Anti-AI-Slop Directives

  1. STALE FEATURES: VOLUME_SYNC_GRACE_PERIOD_MS in constants/spotify.ts appears to be unused after the changes to SpotifyControls.tsx. It should be deleted.
  2. CODE RATIO: The prevActiveIdRef logic in SpotifyControls seems loosely connected. Ensure strictly necessary code is kept. Definitely delete the stale constant mentioned above.
  3. TEST HYGIENE: jest.useFakeTimers() is used in SpotifyControls.test.tsx without a guaranteed cleanup mechanism (like try/finally or afterEach). If a test fails, subsequent tests will hang or fail due to stuck timers.

File-by-File Analysis

app/client/control/components/SpotifyControls.tsx

  • Problem: Throttle recreation on render.
  • Implementation Sample:
    // Use a ref to always access the latest version of the function without recreating the throttled wrapper
    const sendVolumeCommandRef = useRef(sendVolumeCommand);
    useEffect(() => {
      sendVolumeCommandRef.current = sendVolumeCommand;
    });
    
    // Create the throttled function exactly once
    const throttledSendVolumeCommand = useMemo(
      () => throttle((val: number) => sendVolumeCommandRef.current(val), 200),
      []
    );

tests/unit/app/client/control/components/SpotifyControls.test.tsx

  • Problem: Test isolation risk with timers.
  • Implementation Sample:
    afterEach(() => {
      jest.useRealTimers();
    });
    // OR wrap test body in try/finally

constants/spotify.ts

  • Problem: Unused constant.
  • Action: Remove VOLUME_SYNC_GRACE_PERIOD_MS.

Review automatically published via RepoAuditor.

@arii arii added bug Something isn't working refactor tests labels Mar 3, 2026
@arii
Copy link
Owner Author

arii commented Mar 3, 2026

🤖 AI Technical Audit

Code Review: PR #9447 - Fix Spotify Polling crash and cleanup UI slop

This PR addresses a critical server-side crash in the Spotify polling service and refactors volume control logic in the UI. While the crash fix and optimistic updates are valuable, there are issues regarding resource cleanup (throttling), dead code retention, and state mutation patterns.

ANTI-AI-SLOP DIRECTIVES

  1. STALE FEATURES: The constant VOLUME_SYNC_GRACE_PERIOD_MS is removed from SpotifyControls.tsx but remains defined in constants/spotify.ts. It must be deleted.
  2. OVERLY VERBOSE COMMENTS: In services/spotifyPolling.ts, comments like // 1. Immediate Optimistic Broadcast... and // 3. Revert state on failure describe the code's obvious intent. Remove them to let the code speak for itself.
  3. CODE RATIO: The file constants/spotify.ts can be reduced by 1 line (removing the unused constant). The eslint-disable line in SpotifyControls.tsx should be resolved properly rather than suppressed.
  4. OVER-ENGINEERING: The optimistic update logic in SpotifyPolling manually mutates state and handles reverts. If the polling interval is being reduced to 300ms (as seen in the diff), the complexity of manual optimistic updates might outweigh the benefits, especially since this.state mutation risks side effects.

File-by-File Analysis

app/client/control/components/SpotifyControls.tsx

Problem: Unmanaged Throttle Side Effects
Using throttle inside useCallback creates a throttled function, but lodash.throttle (implied by usage) uses internal timers. If the component unmounts while a trailing throttle call is pending, it will attempt to execute sendVolumeCommand on an unmounted component. Additionally, the eslint-disable-next-line suggests the dependencies are not being tracked correctly by the linter.

Implementation Sample:

// proper cleanup of throttle
const throttledSendVolumeCommand = useMemo(
  () => throttle((val: number) => sendVolumeCommand(val), 200),
  [sendVolumeCommand]
);

useEffect(() => {
  return () => {
    throttledSendVolumeCommand.cancel();
  };
}, [throttledSendVolumeCommand]);

constants/spotify.ts

Problem: Dead Code
VOLUME_SYNC_GRACE_PERIOD_MS is no longer used in the codebase but was left in the file.

Implementation Sample:

// Remove this line
export const VOLUME_SYNC_GRACE_PERIOD_MS = 600

services/spotifyPolling.ts

Problem: Direct State Mutation
The code directly mutates this.state.playback.is_playing. Depending on how this.state is managed (e.g., if it's a reference shared with other parts of the application or an immutable store), this could lead to inconsistent states or lack of re-render triggers elsewhere. The crash fix (checking && playback) is correct, but the mutation pattern is risky.

Problem: Magic Numbers
The polling delay was changed from 500 to 300 without a constant. Define this as a constant like SPOTIFY_POLL_DELAY_MS.

Architectural Impact

  • Optimistic UI: Moving optimistic logic to the backend polling service (SpotifyPolling.ts) is an interesting choice. Usually, optimistic updates happen on the client to mask network latency. Doing it in the polling service only benefits other clients connected via WebSocket, not necessarily the sender (who already has local state). Ensure this doesn't introduce race conditions where the server broadcasts a "fake" active state that contradicts the actual Spotify API state fetched 300ms later.

Best Practices

  • TypeScript: The null check fix this.state?.playback correctly addresses the reported crash.
  • Accessibility: Adding getAriaValueText to VolumeSlider is a good a11y improvement.

GitHub Checks

  • Status: No checks found.
  • Risk: Without tests running, the changes to SpotifyPolling (a core service) are high risk. Ensure unit tests cover the new optimistic revert logic.

Review automatically published via RepoAuditor.

- Update `throttledSendVolumeCommand` to use `.cancel()` on unmount.
- Remove verbose slop comments in `services/spotifyPolling.ts`.
- Ensure `jest.useRealTimers()` in `SpotifyControls.test.tsx` runs in `afterEach`.
- Remove stale `VOLUME_SYNC_GRACE_PERIOD_MS` constant.

Co-authored-by: arii <342438+arii@users.noreply.github.com>
@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

🤖 Gemini Review Skipped

Review was skipped for the following reason: Gemini review is disabled

🤖 Manual Trigger Quick Reference

Command Action
@gemini-bot Run AI Code Review (PR only)
@gemini-triage Run Issue Triage
@gemini-coder <task> Generate Code
@create-review-issues Create issues from review (PR only)
@gemini-help Show this help message

Manual Trigger Guide

@arii arii added not reviewed The review could not be performed or completed successfully. and removed request-changes not reviewed The review could not be performed or completed successfully. labels Mar 3, 2026
@arii
Copy link
Owner Author

arii commented Mar 3, 2026

🤖 AI Technical Audit

Deep Review: PR #9447

This PR simplifies the Spotify volume synchronization logic and introduces optimistic updates for playback state. While the cleanup in SpotifyControls is solid, the changes in SpotifyDisplay introduce a UI regression (race condition). Additionally, there is an opportunity to reduce bundle size and duplicated logic.

⚠️ Critical Issues

  1. Regression in SpotifyDisplay.tsx: You removed the VOLUME_SYNC_GRACE_PERIOD_MS logic but did not implement the new SYNC_LOCK_DURATION logic used in SpotifyControls.

    • Impact: When a user releases the slider on SpotifyDisplay, isSliding becomes false immediately. If the WebSocket update hasn't arrived (network latency), the slider will "jump" back to the old server value, then jump again to the new value once the update arrives.
    • Fix: You must implement the same lastUserInteractionRef locking mechanism in SpotifyDisplay or, better yet, share the logic via a hook.
  2. State Mutation in SpotifyPolling.ts:

    • You are directly mutating this.state.playback.is_playing = .... If this.state is treated as immutable elsewhere (or if React components rely on strict equality checks of the state object after a broadcast), this direct mutation might not trigger updates or could cause side effects.
    • Recommendation: Use a shallow clone: const newState = { ...this.state, playback: { ...this.state.playback, is_playing: command === 'PLAY' } }.

📂 File-by-File Analysis

app/client/control/components/SpotifyControls.tsx

  • Improvement: The simplification of the sync lock using SYNC_LOCK_DURATION is much cleaner than the previous pending flag logic.
  • Issue: Adding lodash.throttle.
    • Why: Importing the full lodash library or even just the function adds unnecessary bundle weight for a simple 200ms throttle.
    • Fix: Use a lightweight utility or custom hook.

components/SpotifyDisplay.tsx

  • Problem: Missing sync lock logic (see Critical Issues).

services/spotifyPolling.ts

  • Improvement: The check if (... && playback) correctly prevents the crash mentioned in the PR description.

ANTI-AI-SLOP DIRECTIVES

  1. OVERLY VERBOSE COMMENTS:
    • SpotifyControls.tsx: // Create the throttled function exactly once -> Delete this. useMemo with [] deps implies this.
  2. OVER-ENGINEERING:
    • Using lodash.throttle instead of a simple native implementation or existing project utility.
  3. DUPLICATE HOOKS/TYPES:
    • The volume control logic (throttling, locking, syncing) is now duplicated (and divergent) between SpotifyControls and SpotifyDisplay. This logic should be extracted to useSpotifyVolume(initialVolume, deviceId).
  4. CODE RATIO:
    • Extracting the volume logic to a hook will remove ~30 lines of duplicate state/effect management from both components.
  5. STALE FEATURES:
    • Correctly identified removal of VOLUME_SYNC_GRACE_PERIOD_MS.

✅ Automated Checks

No checks found. Please ensure unit tests pass locally, especially the new lock duration tests.

Review automatically published via RepoAuditor.

@google-labs-jules
Copy link
Contributor

🤖 AI Technical Audit

Code Review: Stability Fixes & Cleanup

This PR effectively addresses the server-side crash in spotifyPolling.ts and simplifies the volume synchronization logic in the UI. The removal of the complex hasPendingSendRef state in favor of a time-based lock is a significant improvement in readability and robustness.

Critical Issues

1. Broken Throttle Implementation (SpotifyControls.tsx)
The introduction of throttle is currently ineffective due to React dependency stability.

throttledSendVolumeCommand depends on sendVolumeCommand. sendVolumeCommand depends on resolveTargetDeviceId. If resolveTargetDeviceId (or its dependencies like activeDevice from polling updates) changes on re-renders, sendVolumeCommand is recreated.

When sendVolumeCommand is recreated:

  1. throttledSendVolumeCommand is recreated.
  2. A new lodash.throttle instance is created.
  3. The internal timer/state of the throttle is lost.
  4. The new throttle instance fires immediately (leading edge default).

Result: The volume command is likely sent on every slider move if the component re-renders (which happens often with sliders or polling), defeating the purpose of throttling.

Fix: Use a useRef to hold the latest sendVolumeCommand and create the throttle function only once.

Anti-AI-Slop Directives

  1. STALE FEATURES: VOLUME_SYNC_GRACE_PERIOD_MS in constants/spotify.ts appears to be unused after the changes to SpotifyControls.tsx. It should be deleted.
  2. CODE RATIO: The prevActiveIdRef logic in SpotifyControls seems loosely connected. Ensure strictly necessary code is kept. Definitely delete the stale constant mentioned above.
  3. TEST HYGIENE: jest.useFakeTimers() is used in SpotifyControls.test.tsx without a guaranteed cleanup mechanism (like try/finally or afterEach). If a test fails, subsequent tests will hang or fail due to stuck timers.

File-by-File Analysis

app/client/control/components/SpotifyControls.tsx

  • Problem: Throttle recreation on render.
  • Implementation Sample:
    // Use a ref to always access the latest version of the function without recreating the throttled wrapper
    const sendVolumeCommandRef = useRef(sendVolumeCommand);
    useEffect(() => {
      sendVolumeCommandRef.current = sendVolumeCommand;
    });
    
    // Create the throttled function exactly once
    const throttledSendVolumeCommand = useMemo(
      () => throttle((val: number) => sendVolumeCommandRef.current(val), 200),
      []
    );

tests/unit/app/client/control/components/SpotifyControls.test.tsx

  • Problem: Test isolation risk with timers.
  • Implementation Sample:
    afterEach(() => {
      jest.useRealTimers();
    });
    // OR wrap test body in try/finally

constants/spotify.ts

  • Problem: Unused constant.
  • Action: Remove VOLUME_SYNC_GRACE_PERIOD_MS.

Review automatically published via RepoAuditor.

I have addressed all the feedback in your review. Specifically, I've updated throttledSendVolumeCommand to use the useRef and useMemo pattern with a cleanup useEffect on unmount. I also removed the stale VOLUME_SYNC_GRACE_PERIOD_MS constant from constants/spotify.ts, ensured jest.useRealTimers() is properly called within an afterEach block in the test suite, and removed the overly verbose comments in services/spotifyPolling.ts. These fixes are now committed to the branch.

- Use `useRef` and `useMemo` for `throttledSendVolumeCommand` in `SpotifyControls.tsx` to fix React dependency issues, and cancel the throttle on unmount.
- Add `jest.useRealTimers()` inside a global `afterEach` block in `SpotifyControls.test.tsx` to improve test hygiene.
- Remove the unused `VOLUME_SYNC_GRACE_PERIOD_MS` from `constants/spotify.ts` and `SpotifyDisplay.tsx`.
- Strip away verbose comment slop from `services/spotifyPolling.ts`.

Co-authored-by: arii <342438+arii@users.noreply.github.com>
@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

🤖 Gemini Review Skipped

Review was skipped for the following reason: Gemini review is disabled

🤖 Manual Trigger Quick Reference

Command Action
@gemini-bot Run AI Code Review (PR only)
@gemini-triage Run Issue Triage
@gemini-coder <task> Generate Code
@create-review-issues Create issues from review (PR only)
@gemini-help Show this help message

Manual Trigger Guide

@arii arii added not reviewed The review could not be performed or completed successfully. and removed request-changes not reviewed The review could not be performed or completed successfully. labels Mar 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working not reviewed The review could not be performed or completed successfully. refactor refactor-required risk: high tests ui-regression

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant