Skip to content

Commit 2ab08cc

Browse files
BrsktInrixia
authored andcommitted
Add debounce to Discord RPC activity updates
Prevents rate-limit errors when rapidly changing tracks by: - Debouncing calls (300ms delay) - Queueing updates if one is already in progress
1 parent 0f15aed commit 2ab08cc

2 files changed

Lines changed: 55 additions & 7 deletions

File tree

plugins/DiscordRPC/src/index.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,10 @@ export const { trace, errSignal } = Tracer("[DiscordRPC]");
99
export { Settings } from "./Settings";
1010

1111
redux.intercept(["playbackControls/SEEK", "playbackControls/SET_PLAYBACK_STATE"], unloads, () => {
12-
updateActivity()
13-
.then(() => (errSignal!._ = undefined))
14-
.catch(trace.err.withContext("Failed to set activity"));
12+
updateActivity();
1513
});
1614
MediaItem.onMediaTransition(unloads, (mediaItem) => {
17-
updateActivity(mediaItem)
18-
.then(() => (errSignal!._ = undefined))
19-
.catch(trace.err.withContext("Failed to set activity"));
15+
updateActivity(mediaItem);
2016
});
2117
unloads.add(cleanupRPC.bind(cleanupRPC));
2218

plugins/DiscordRPC/src/updateActivity.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,63 @@ import type { SetActivity } from "@xhayper/discord-rpc";
44
import { fmtStr, getStatusText } from "./activityTextHelpers";
55
import { setActivity, StatusDisplayTypeEnum } from "./discord.native";
66
import { settings } from "./Settings";
7+
import { trace, errSignal } from "./index";
78

89
// Proxy this so we dont try import a node native module
910
const StatusDisplayType = await StatusDisplayTypeEnum();
1011

11-
export const updateActivity = async (mediaItem?: MediaItem) => {
12+
// Debounce state
13+
const DEBOUNCE_MS = 300;
14+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
15+
let isUpdating = false;
16+
let pendingUpdate: MediaItem | undefined | null = null; // null = no pending, undefined = pending with no mediaItem
17+
18+
/**
19+
* Debounced wrapper for _updateActivity
20+
* - Waits DEBOUNCE_MS after last call before executing
21+
* - If already updating, queues the next update
22+
*/
23+
export const updateActivity = (mediaItem?: MediaItem) => {
24+
// If currently updating, mark that we need another update after
25+
if (isUpdating) {
26+
pendingUpdate = mediaItem;
27+
return;
28+
}
29+
30+
// Clear existing timer
31+
if (debounceTimer) clearTimeout(debounceTimer);
32+
33+
// Set new debounce timer
34+
debounceTimer = setTimeout(async () => {
35+
debounceTimer = null;
36+
await executeUpdate(mediaItem);
37+
}, DEBOUNCE_MS);
38+
};
39+
40+
/**
41+
* Execute the actual update with mutex protection
42+
*/
43+
const executeUpdate = async (mediaItem?: MediaItem) => {
44+
isUpdating = true;
45+
try {
46+
await _updateActivity(mediaItem);
47+
errSignal!._ = undefined;
48+
} catch (e) {
49+
trace.err.withContext("Failed to set activity")(e);
50+
} finally {
51+
isUpdating = false;
52+
if (pendingUpdate !== null) {
53+
const pending = pendingUpdate;
54+
pendingUpdate = null;
55+
updateActivity(pending);
56+
}
57+
}
58+
};
59+
60+
/**
61+
* Internal update implementation (no debounce/mutex)
62+
*/
63+
const _updateActivity = async (mediaItem?: MediaItem) => {
1264
if (!PlayState.playing && !settings.displayOnPause) return await setActivity();
1365

1466
mediaItem ??= await MediaItem.fromPlaybackContext();

0 commit comments

Comments
 (0)