Skip to content

Conversation

@GJRobert
Copy link
Collaborator

Summary

Changed the cloud synchronization mechanism from debounce to throttle to ensure more reliable and consistent uploads during playback, while still reducing API call frequency.

Key Changes

  • Replaced debounce with throttle: Changed from a single timer that resets on every change to a throttle mechanism that guarantees syncs at least every 30 seconds
  • Added dual-timer system:
    • Main throttle timer ensures periodic uploads during active playback
    • Trailing timer captures final state after user stops making changes
  • Improved state tracking: Added lastSyncTime to track when the last sync occurred and calculate remaining time until next sync
  • Enhanced page visibility handling: Updated cleanup logic to clear both throttle and trailing timers when page becomes hidden

Implementation Details

  • Sync interval set to 30 seconds (SYNC_INTERVAL = 30000)
  • If 30+ seconds have passed since last sync, upload immediately
  • Otherwise, schedule upload for the remaining time until 30 seconds elapse
  • Trailing timer (also 30 seconds) ensures final state is captured after user stops interacting
  • When page becomes hidden, immediately sync any pending changes and clear all timers

Benefits

  • More predictable sync behavior during playback
  • Guarantees uploads at regular intervals rather than only on activity
  • Captures final playback state when user stops playing
  • Maintains reduced API call frequency compared to per-change syncing

原本的 debounce 機制會在每次觸發時重置計時器,導致持續播放時
永遠無法達到 30 秒的等待時間,書籤只有在停止播放後才會上傳。

現改用 throttle 機制:
- 播放期間至少每 30 秒上傳一次
- 設置尾隨計時器確保停止播放後最終狀態也會上傳
- 維持頁面隱藏時的緊急上傳功能
@gemini-code-assist
Copy link

Summary of Changes

Hello @GJRobert, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request refactors the cloud synchronization logic from a debounce pattern to a more robust throttle implementation. The primary goal is to guarantee consistent data uploads at regular intervals during continuous activity, such as playback, while also ensuring that the final state is synchronized once activity ceases. This change aims to improve data integrity and user experience by providing more predictable sync behavior without excessively increasing API call frequency.

Highlights

  • Synchronization Mechanism: The cloud synchronization mechanism has been changed from a debounce function to a throttle function to ensure more consistent and regular uploads during playback.
  • Dual-Timer System: A new dual-timer system has been implemented, consisting of a main throttle timer for periodic uploads and a trailing timer to capture the final state after user interaction stops.
  • State Tracking: Improved state tracking was introduced with lastSyncTime and a SYNC_INTERVAL of 30 seconds to manage sync timing and guarantee uploads occur at least every 30 seconds.
  • Page Visibility Handling: The cleanup logic in setupPageUnloadSync has been updated to correctly clear both the throttle and trailing timers and perform an immediate sync when the page becomes hidden.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@claude
Copy link

claude bot commented Jan 25, 2026

Pull Request Review: Replace debounce with throttle for cloud sync

Overview

This PR changes the cloud synchronization mechanism from debounce to throttle, adding a dual-timer system to ensure more reliable uploads during playback. The implementation is generally solid, but there are several areas that need attention.

Code Quality & Best Practices

✅ Strengths

  • Clear intent: The dual-timer approach (throttle + trailing) effectively addresses the problem of capturing both periodic updates and final state
  • Good documentation: Comments explain the logic in both Chinese and the code structure is clear
  • Proper cleanup: Timer cleanup in visibility change handler is comprehensive

⚠️ Issues Found

1. Race Condition: hasPendingSync flag management (js/cloud-sync.js:418-422)

The trailing timer checks hasPendingSync but doesn't consider that a sync may have just completed via the throttle timer:

syncTrailingTimer = setTimeout(() => {
  syncTrailingTimer = null;
  if (hasPendingSync) {  // ⚠️ This may be stale
    lastSyncTime = Date.now();
    syncToCloud();
    hasPendingSync = false;
  }
}, SYNC_INTERVAL);

Scenario:

  1. User makes change at T=0 → throttle timer set for T=30
  2. User makes change at T=29 → trailing timer set for T=59
  3. Throttle fires at T=30, sets hasPendingSync = false
  4. Trailing fires at T=59, sees hasPendingSync = false, skips sync
  5. Change from T=29 is lost ❌

Recommendation: The trailing timer should track whether new changes arrived after the last sync, not rely on the shared hasPendingSync flag.

2. Double sync potential (js/cloud-sync.js:393-403)

When now - lastSyncTime >= SYNC_INTERVAL, the code immediately syncs and also sets up a new trailing timer. If the user stops making changes right after, the trailing timer will fire unnecessarily 30 seconds later.

Recommendation: Clear or don't set the trailing timer when immediate sync occurs.

3. Initial state bug (js/cloud-sync.js:377)

lastSyncTime is initialized to 0. This means the first call to triggerCloudSync() will always trigger an immediate sync because Date.now() - 0 is always >= 30000ms. This defeats the throttling purpose for the first sync.

Recommendation: Initialize to Date.now() or handle the zero case explicitly.

Potential Bugs

4. Missing error handling in visibility change (js/cloud-sync.js:432-445)

The visibility change handler calls syncToCloud() without try/catch. If the sync fails, the hasPendingSync flag is still set to false, potentially losing data.

5. Asynchronous sync timing issue

syncToCloud() is async, but the code sets hasPendingSync = false synchronously before the upload completes. If syncToCloud() fails, the flag will incorrectly indicate no pending changes.

Recommendation: Only set hasPendingSync = false after successful sync completion.

Performance Considerations

✅ Good

  • Throttling to 30 seconds significantly reduces API calls compared to per-change syncing
  • Timer cleanup prevents memory leaks

⚠️ Concerns

  • Trailing timer always runs full duration: Every change resets the 30-second trailing timer. During active playback, this timer continuously restarts, which is fine, but the logic could be more efficient.

Security Concerns

✅ No major security issues

  • No new security vulnerabilities introduced
  • Existing code already handles authentication checks

💡 Minor note

  • The localStorage data (hakkaBookmarks) is parsed without additional validation before upload, but this is pre-existing and not introduced by this PR.

Test Coverage

Critical: No tests found

This PR modifies complex timing logic with multiple edge cases, but there are no automated tests in the repository for this functionality.

Recommended test cases:

  1. Rapid successive changes within 30s → only one sync occurs
  2. Change, wait 30s, change again → two syncs occur
  3. Continuous changes over 60s → exactly 2 syncs occur (at 30s and 60s)
  4. Change, then stop → trailing timer captures final state after 30s
  5. Page visibility change with pending sync → immediate sync occurs
  6. All timers properly cleared when page hidden

Summary

Overall Assessment: The approach is sound and addresses the stated goal, but the implementation has several bugs that could cause data loss or unnecessary syncs.

Must Fix Before Merge

  1. ⚠️ Race condition with hasPendingSync flag between throttle and trailing timers
  2. ⚠️ Initial sync timing issue (lastSyncTime = 0)
  3. ⚠️ Async handling of hasPendingSync flag
  4. ⚠️ Error handling in visibility change handler

Should Fix

  1. Prevent double sync when immediate sync + trailing timer overlap
  2. Add basic test coverage for timing logic

Nice to Have

  1. Consider using a more robust state machine approach for sync state management

Suggested Fix for Main Issue

let syncThrottleTimer = null;
let syncTrailingTimer = null;
let hasPendingSync = false;
let lastSyncTime = Date.now(); // Initialize to now, not 0
const SYNC_INTERVAL = 30000;

async function triggerCloudSync() {
  if (!cloudSyncState.isLoggedIn) return;

  hasPendingSync = true;
  const now = Date.now();

  // Clear trailing timer
  if (syncTrailingTimer) {
    clearTimeout(syncTrailingTimer);
    syncTrailingTimer = null;
  }

  // Immediate sync if interval elapsed
  if (now - lastSyncTime >= SYNC_INTERVAL) {
    if (syncThrottleTimer) {
      clearTimeout(syncThrottleTimer);
      syncThrottleTimer = null;
    }

    lastSyncTime = now;
    await syncToCloud(); // Make it await
    hasPendingSync = false;
    return; // Don't set trailing timer after immediate sync
  } else if (!syncThrottleTimer) {
    const remainingTime = SYNC_INTERVAL - (now - lastSyncTime);
    syncThrottleTimer = setTimeout(async () => {
      syncThrottleTimer = null;
      lastSyncTime = Date.now();
      await syncToCloud();
      hasPendingSync = false;
    }, remainingTime);
  }

  // Always set trailing timer
  syncTrailingTimer = setTimeout(async () => {
    syncTrailingTimer = null;
    if (hasPendingSync) { // This will now be more reliable
      lastSyncTime = Date.now();
      await syncToCloud();
      hasPendingSync = false;
    }
  }, SYNC_INTERVAL);
}

Would you like me to help implement these fixes?


🤖 Generated with Claude Code

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

這個 PR 成功地將雲端同步機制從 debounce 改為更適合的 throttle 方法。新的實作採用了節流(throttle)和尾隨(trailing)計時器的雙計時器系統,這個設計相當周全,既能確保在使用者操作期間能定期上傳資料,也能捕捉到最終狀態,從而提升了資料的可靠性。

我的審查意見包含一個重構 triggerCloudSync 函式的建議,以減少重複的程式碼,進而提升可維護性。

另外,有一個重要的潛在風險需要考慮:競爭條件(race condition)。新的節流邏輯可能會在先前的 syncToCloud 呼叫尚未完成時,再次觸發它。由於 syncToCloud 函式本身似乎沒有像 syncFromCloud 那樣的並行鎖(concurrency lock),這可能導致多個上傳請求同時進行。我建議在 syncToCloud 中加入鎖定機制(例如使用像 isSyncing 這樣的旗標),以確保一次只有一個上傳在執行,讓同步過程更加穩健。

Comment on lines 380 to 424
function triggerCloudSync() {
if (!cloudSyncState.isLoggedIn) return;

hasPendingSync = true;
const now = Date.now();

if (syncDebounceTimer) {
clearTimeout(syncDebounceTimer);
// 清除尾隨計時器(因為有新的變更進來)
if (syncTrailingTimer) {
clearTimeout(syncTrailingTimer);
syncTrailingTimer = null;
}

syncDebounceTimer = setTimeout(() => {
// 如果距離上次同步已超過 30 秒,立即同步
if (now - lastSyncTime >= SYNC_INTERVAL) {
// 清除節流計時器(如果有的話)
if (syncThrottleTimer) {
clearTimeout(syncThrottleTimer);
syncThrottleTimer = null;
}

lastSyncTime = now;
syncToCloud();
hasPendingSync = false;
}, 30000); // 30 秒後同步,減少 Supabase API 呼叫次數
} else if (!syncThrottleTimer) {
// 還沒到 30 秒,且沒有計時器在跑,設置計時器
const remainingTime = SYNC_INTERVAL - (now - lastSyncTime);
syncThrottleTimer = setTimeout(() => {
syncThrottleTimer = null;
lastSyncTime = Date.now();
syncToCloud();
hasPendingSync = false;
}, remainingTime);
}

// 設置尾隨計時器:確保最後一次變更後也會上傳
// 這樣停止播放後的最終狀態也能被保存
syncTrailingTimer = setTimeout(() => {
syncTrailingTimer = null;
if (hasPendingSync) {
lastSyncTime = Date.now();
syncToCloud();
hasPendingSync = false;
}
}, SYNC_INTERVAL);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

這段程式碼實現了帶有前緣和後緣觸發的節流邏輯,功能看起來是正確的。不過,在 if 判斷式、syncThrottleTimersyncTrailingTimer 的回呼函式中,都有重複的同步邏輯(更新 lastSyncTime、呼叫 syncToCloud()、重設 hasPendingSync)。同樣的重複邏輯也出現在 setupPageUnloadSync 函式中。

為了提高程式碼的可讀性和可維護性,並遵循 DRY (Don't Repeat Yourself) 原則,建議將這段重複的邏輯提取到一個輔助函式中。這樣可以讓 triggerCloudSync 的主要邏輯更清晰,也更容易在未來統一修改同步行為。

我建議的重構如下:

function triggerCloudSync() {
  if (!cloudSyncState.isLoggedIn) return;

  const executeSync = (syncTime) => {
    lastSyncTime = syncTime;
    syncToCloud();
    hasPendingSync = false;
  };

  hasPendingSync = true;
  const now = Date.now();

  // 清除尾隨計時器(因為有新的變更進來)
  if (syncTrailingTimer) {
    clearTimeout(syncTrailingTimer);
    syncTrailingTimer = null;
  }

  // 如果距離上次同步已超過 30 秒,立即同步
  if (now - lastSyncTime >= SYNC_INTERVAL) {
    // 清除節流計時器(如果有的話)
    if (syncThrottleTimer) {
      clearTimeout(syncThrottleTimer);
      syncThrottleTimer = null;
    }
    executeSync(now);
  } else if (!syncThrottleTimer) {
    // 還沒到 30 秒,且沒有計時器在跑,設置計時器
    const remainingTime = SYNC_INTERVAL - (now - lastSyncTime);
    syncThrottleTimer = setTimeout(() => {
      syncThrottleTimer = null;
      executeSync(Date.now());
    }, remainingTime);
  }

  // 設置尾隨計時器:確保最後一次變更後也會上傳
  // 這樣停止播放後的最終狀態也能被保存
  syncTrailingTimer = setTimeout(() => {
    syncTrailingTimer = null;
    if (hasPendingSync) {
      executeSync(Date.now());
    }
  }, SYNC_INTERVAL);
}

@GJRobert GJRobert merged commit d797e84 into main Jan 25, 2026
4 checks passed
@GJRobert GJRobert deleted the claude/fix-bookmark-sync-playback-FDVZ7 branch January 25, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants