Debounce Android realtime refresh bursts
This commit is contained in:
@@ -2,6 +2,8 @@ package com.hyzq.boss;
|
|||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
@@ -20,6 +22,7 @@ public class ConversationFolderActivity extends BossScreenActivity {
|
|||||||
public static final String EXTRA_TARGET_PROJECT_ID = "target_project_id";
|
public static final String EXTRA_TARGET_PROJECT_ID = "target_project_id";
|
||||||
public static final String EXTRA_TARGET_PROJECT_IDS = "target_project_ids";
|
public static final String EXTRA_TARGET_PROJECT_IDS = "target_project_ids";
|
||||||
public static final String EXTRA_TARGET_PROJECT_LABEL = "target_project_label";
|
public static final String EXTRA_TARGET_PROJECT_LABEL = "target_project_label";
|
||||||
|
private static final long REALTIME_REFRESH_DEBOUNCE_MS = 300L;
|
||||||
private static final long REALTIME_RELOAD_THROTTLE_MS = 900L;
|
private static final long REALTIME_RELOAD_THROTTLE_MS = 900L;
|
||||||
|
|
||||||
private String folderKey;
|
private String folderKey;
|
||||||
@@ -29,8 +32,17 @@ public class ConversationFolderActivity extends BossScreenActivity {
|
|||||||
private ArrayList<String> targetProjectIds;
|
private ArrayList<String> targetProjectIds;
|
||||||
private String targetProjectLabel;
|
private String targetProjectLabel;
|
||||||
private @Nullable BossRealtimeClient realtimeClient;
|
private @Nullable BossRealtimeClient realtimeClient;
|
||||||
|
private final Handler uiHandler = new Handler(Looper.getMainLooper());
|
||||||
private final Map<String, Long> recentRealtimeEventTimestamps = new LinkedHashMap<>();
|
private final Map<String, Long> recentRealtimeEventTimestamps = new LinkedHashMap<>();
|
||||||
private final Set<String> trackedProjectIds = new LinkedHashSet<>();
|
private final Set<String> trackedProjectIds = new LinkedHashSet<>();
|
||||||
|
private boolean realtimeReloadScheduled;
|
||||||
|
private final Runnable realtimeReloadRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
realtimeReloadScheduled = false;
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
@@ -67,12 +79,14 @@ public class ConversationFolderActivity extends BossScreenActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
|
cancelRealtimeReloadSchedule();
|
||||||
stopRealtimeUpdates();
|
stopRealtimeUpdates();
|
||||||
super.onPause();
|
super.onPause();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
|
cancelRealtimeReloadSchedule();
|
||||||
stopRealtimeUpdates();
|
stopRealtimeUpdates();
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
@@ -130,7 +144,20 @@ public class ConversationFolderActivity extends BossScreenActivity {
|
|||||||
if (isDuplicateRealtimeEvent(eventFingerprint, now)) {
|
if (isDuplicateRealtimeEvent(eventFingerprint, now)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
runOnUiThread(this::reload);
|
runOnUiThread(this::scheduleRealtimeReload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleRealtimeReload() {
|
||||||
|
if (realtimeReloadScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
realtimeReloadScheduled = true;
|
||||||
|
uiHandler.postDelayed(realtimeReloadRunnable, REALTIME_REFRESH_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelRealtimeReloadSchedule() {
|
||||||
|
uiHandler.removeCallbacks(realtimeReloadRunnable);
|
||||||
|
realtimeReloadScheduled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldReloadForRealtimeEvent(BossRealtimeEvent event) {
|
private boolean shouldReloadForRealtimeEvent(BossRealtimeEvent event) {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private static final String KEY_LAST_ROOT_TAB = "last_root_tab";
|
private static final String KEY_LAST_ROOT_TAB = "last_root_tab";
|
||||||
private static final long ROOT_BACK_EXIT_WINDOW_MS = 1_500L;
|
private static final long ROOT_BACK_EXIT_WINDOW_MS = 1_500L;
|
||||||
private static final long CONVERSATION_AUTO_REFRESH_MS = 12_000L;
|
private static final long CONVERSATION_AUTO_REFRESH_MS = 12_000L;
|
||||||
|
private static final long REALTIME_REFRESH_DEBOUNCE_MS = 350L;
|
||||||
private static final long REALTIME_REFRESH_THROTTLE_MS = 900L;
|
private static final long REALTIME_REFRESH_THROTTLE_MS = 900L;
|
||||||
|
|
||||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
@@ -106,6 +107,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private boolean conversationAutoRefreshEnabled = false;
|
private boolean conversationAutoRefreshEnabled = false;
|
||||||
private boolean rootTabRefreshInFlight = false;
|
private boolean rootTabRefreshInFlight = false;
|
||||||
private boolean pendingRootTabRefresh = false;
|
private boolean pendingRootTabRefresh = false;
|
||||||
|
private boolean realtimeRefreshScheduled = false;
|
||||||
private final java.util.HashMap<String, Long> recentRealtimeEventTimestamps = new java.util.HashMap<>();
|
private final java.util.HashMap<String, Long> recentRealtimeEventTimestamps = new java.util.HashMap<>();
|
||||||
private final Set<String> selectedConversationProjectIds = new LinkedHashSet<>();
|
private final Set<String> selectedConversationProjectIds = new LinkedHashSet<>();
|
||||||
private @Nullable RootPagerAdapter rootPagerAdapter;
|
private @Nullable RootPagerAdapter rootPagerAdapter;
|
||||||
@@ -123,6 +125,16 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
armConversationAutoRefresh();
|
armConversationAutoRefresh();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
private final Runnable realtimeRefreshRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
realtimeRefreshScheduled = false;
|
||||||
|
if (contentPanel == null || contentPanel.getVisibility() != View.VISIBLE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshCurrentTab();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
@@ -178,6 +190,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
cancelConversationAutoRefresh();
|
cancelConversationAutoRefresh();
|
||||||
|
cancelRealtimeRefreshSchedule();
|
||||||
stopRealtimeUpdates();
|
stopRealtimeUpdates();
|
||||||
executor.shutdownNow();
|
executor.shutdownNow();
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
@@ -195,6 +208,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
conversationAutoRefreshEnabled = false;
|
conversationAutoRefreshEnabled = false;
|
||||||
cancelConversationAutoRefresh();
|
cancelConversationAutoRefresh();
|
||||||
|
cancelRealtimeRefreshSchedule();
|
||||||
stopRealtimeUpdates();
|
stopRealtimeUpdates();
|
||||||
super.onPause();
|
super.onPause();
|
||||||
}
|
}
|
||||||
@@ -567,7 +581,20 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
if (isDuplicateRealtimeEvent(eventFingerprint, now)) {
|
if (isDuplicateRealtimeEvent(eventFingerprint, now)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
runOnUiThread(this::refreshCurrentTab);
|
runOnUiThread(this::scheduleRealtimeRefresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleRealtimeRefresh() {
|
||||||
|
if (realtimeRefreshScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
realtimeRefreshScheduled = true;
|
||||||
|
uiHandler.postDelayed(realtimeRefreshRunnable, REALTIME_REFRESH_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelRealtimeRefreshSchedule() {
|
||||||
|
uiHandler.removeCallbacks(realtimeRefreshRunnable);
|
||||||
|
realtimeRefreshScheduled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {
|
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
|||||||
public static final String EXTRA_PROJECT_ID = "project_id";
|
public static final String EXTRA_PROJECT_ID = "project_id";
|
||||||
public static final String EXTRA_PROJECT_NAME = "project_name";
|
public static final String EXTRA_PROJECT_NAME = "project_name";
|
||||||
private static final long CONVERSATION_AUTO_REFRESH_MS = 8_000L;
|
private static final long CONVERSATION_AUTO_REFRESH_MS = 8_000L;
|
||||||
|
private static final long REALTIME_REFRESH_DEBOUNCE_MS = 300L;
|
||||||
private static final long REPLY_WAIT_TIMEOUT_MS = 55_000L;
|
private static final long REPLY_WAIT_TIMEOUT_MS = 55_000L;
|
||||||
private static final long REPLY_WAIT_POLL_INTERVAL_MS = 1_500L;
|
private static final long REPLY_WAIT_POLL_INTERVAL_MS = 1_500L;
|
||||||
private static final long REALTIME_RELOAD_THROTTLE_MS = 900L;
|
private static final long REALTIME_RELOAD_THROTTLE_MS = 900L;
|
||||||
@@ -92,6 +93,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
|||||||
private final Handler uiHandler = new Handler(Looper.getMainLooper());
|
private final Handler uiHandler = new Handler(Looper.getMainLooper());
|
||||||
private boolean conversationAutoRefreshArmed;
|
private boolean conversationAutoRefreshArmed;
|
||||||
private boolean conversationAutoRefreshEnabled;
|
private boolean conversationAutoRefreshEnabled;
|
||||||
|
private boolean realtimeReloadScheduled;
|
||||||
private boolean reloadInFlight;
|
private boolean reloadInFlight;
|
||||||
private boolean pendingReload;
|
private boolean pendingReload;
|
||||||
private boolean pendingReloadForcedScrollToBottom;
|
private boolean pendingReloadForcedScrollToBottom;
|
||||||
@@ -108,6 +110,13 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
|||||||
armConversationAutoRefresh();
|
armConversationAutoRefresh();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
private final Runnable realtimeReloadRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
realtimeReloadScheduled = false;
|
||||||
|
triggerRealtimeReload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
static final class ChromeBindings {
|
static final class ChromeBindings {
|
||||||
final boolean multiSelecting;
|
final boolean multiSelecting;
|
||||||
@@ -278,6 +287,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
cancelConversationAutoRefresh();
|
cancelConversationAutoRefresh();
|
||||||
|
cancelRealtimeReloadSchedule();
|
||||||
stopRealtimeUpdates();
|
stopRealtimeUpdates();
|
||||||
replyWaitExecutor.shutdownNow();
|
replyWaitExecutor.shutdownNow();
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
@@ -295,6 +305,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
|||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
conversationAutoRefreshEnabled = false;
|
conversationAutoRefreshEnabled = false;
|
||||||
cancelConversationAutoRefresh();
|
cancelConversationAutoRefresh();
|
||||||
|
cancelRealtimeReloadSchedule();
|
||||||
stopRealtimeUpdates();
|
stopRealtimeUpdates();
|
||||||
super.onPause();
|
super.onPause();
|
||||||
}
|
}
|
||||||
@@ -324,7 +335,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
|||||||
if (isDuplicateRealtimeEvent(eventFingerprint, now)) {
|
if (isDuplicateRealtimeEvent(eventFingerprint, now)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
runOnUiThread(this::triggerRealtimeReload);
|
runOnUiThread(this::scheduleRealtimeReload);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {
|
private boolean isDuplicateRealtimeEvent(String eventFingerprint, long now) {
|
||||||
@@ -362,6 +373,19 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
|||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void scheduleRealtimeReload() {
|
||||||
|
if (realtimeReloadScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
realtimeReloadScheduled = true;
|
||||||
|
uiHandler.postDelayed(realtimeReloadRunnable, REALTIME_REFRESH_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelRealtimeReloadSchedule() {
|
||||||
|
uiHandler.removeCallbacks(realtimeReloadRunnable);
|
||||||
|
realtimeReloadScheduled = false;
|
||||||
|
}
|
||||||
|
|
||||||
private void reload(boolean forcedScrollToBottom) {
|
private void reload(boolean forcedScrollToBottom) {
|
||||||
if (projectId == null || projectId.isEmpty()) {
|
if (projectId == null || projectId.isEmpty()) {
|
||||||
showMessage("缺少 projectId");
|
showMessage("缺少 projectId");
|
||||||
|
|||||||
49
tests/android-realtime-refresh-debounce.test.ts
Normal file
49
tests/android-realtime-refresh-debounce.test.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
|
||||||
|
async function readSource(path: string) {
|
||||||
|
return readFile(new URL(path, import.meta.url), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
test("MainActivity debounces root realtime refresh bursts", async () => {
|
||||||
|
const source = await readSource("../android/app/src/main/java/com/hyzq/boss/MainActivity.java");
|
||||||
|
|
||||||
|
assert.match(source, /private static final long REALTIME_REFRESH_DEBOUNCE_MS = [\d_]+L;/);
|
||||||
|
assert.match(source, /private boolean realtimeRefreshScheduled(?: = false)?;/);
|
||||||
|
assert.match(source, /private final Runnable realtimeRefreshRunnable = new Runnable\(\)/);
|
||||||
|
assert.match(source, /scheduleRealtimeRefresh\(\)/);
|
||||||
|
assert.doesNotMatch(
|
||||||
|
source,
|
||||||
|
/runOnUiThread\(this::refreshCurrentTab\)/,
|
||||||
|
"root page should coalesce bursts instead of refreshing immediately for each event",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ProjectDetailActivity debounces realtime chat reload bursts", async () => {
|
||||||
|
const source = await readSource("../android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java");
|
||||||
|
|
||||||
|
assert.match(source, /private static final long REALTIME_REFRESH_DEBOUNCE_MS = [\d_]+L;/);
|
||||||
|
assert.match(source, /private boolean realtimeReloadScheduled(?: = false)?;/);
|
||||||
|
assert.match(source, /private final Runnable realtimeReloadRunnable = new Runnable\(\)/);
|
||||||
|
assert.match(source, /scheduleRealtimeReload\(\)/);
|
||||||
|
assert.doesNotMatch(
|
||||||
|
source,
|
||||||
|
/runOnUiThread\(this::triggerRealtimeReload\)/,
|
||||||
|
"chat page should debounce repeated realtime updates before reloading",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ConversationFolderActivity debounces realtime folder reload bursts", async () => {
|
||||||
|
const source = await readSource("../android/app/src/main/java/com/hyzq/boss/ConversationFolderActivity.java");
|
||||||
|
|
||||||
|
assert.match(source, /private static final long REALTIME_REFRESH_DEBOUNCE_MS = [\d_]+L;/);
|
||||||
|
assert.match(source, /private boolean realtimeReloadScheduled(?: = false)?;/);
|
||||||
|
assert.match(source, /private final Runnable realtimeReloadRunnable = new Runnable\(\)/);
|
||||||
|
assert.match(source, /scheduleRealtimeReload\(\)/);
|
||||||
|
assert.doesNotMatch(
|
||||||
|
source,
|
||||||
|
/runOnUiThread\(this::reload\)/,
|
||||||
|
"folder page should debounce repeated realtime updates before reloading",
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user