Skip to content
This repository was archived by the owner on Jun 2, 2026. It is now read-only.

Commit cc6a634

Browse files
committed
feat(performance): Implement callback cleanup mechanism
Add unregister_callback method and track callbacks for cleanup to prevent memory leaks when actions are destroyed. Changes: - backend.py: Add unregister_callback() method - actions/DiscordCore.py: Add callback tracking list - actions/DiscordCore.py: Add register_backend_callback() helper - actions/DiscordCore.py: Add cleanup_callbacks() method - actions/DiscordCore.py: Add __del__() for automatic cleanup - actions/Mute.py: Use register_backend_callback() - actions/Deafen.py: Use register_backend_callback() - actions/TogglePTT.py: Use register_backend_callback() - actions/ChangeVoiceChannel.py: Use register_backend_callback() Impact: Prevents memory leak in long-running sessions Resolves issue #14 from RESEARCH.md Phase 3
1 parent 20bbf50 commit cc6a634

6 files changed

Lines changed: 39 additions & 12 deletions

File tree

actions/ChangeVoiceChannel.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,20 @@ def __init__(self, *args, **kwargs):
2121
super().__init__(*args, **kwargs)
2222
self.has_configuration = True
2323
self._current_channel: str = ""
24-
self.icon_keys = [Icons.VOICE_CHANNEL_ACTIVE,
25-
Icons.VOICE_CHANNEL_INACTIVE]
24+
self.icon_keys = [Icons.VOICE_CHANNEL_ACTIVE, Icons.VOICE_CHANNEL_INACTIVE]
2625
self.current_icon = self.get_icon(Icons.VOICE_CHANNEL_INACTIVE)
2726
self.icon_name = Icons.VOICE_CHANNEL_INACTIVE
2827

2928
def on_ready(self):
3029
super().on_ready()
31-
self.backend.register_callback(
32-
VOICE_CHANNEL_SELECT, self._update_display)
30+
self.register_backend_callback(VOICE_CHANNEL_SELECT, self._update_display)
3331

3432
def _update_display(self, value: dict):
3533
if not self.backend:
3634
self.show_error()
3735
return
3836
self.hide_error()
39-
self._current_channel = value.get(
40-
"channel_id", None) if value else None
37+
self._current_channel = value.get("channel_id", None) if value else None
4138
self.icon_name = (
4239
Icons.VOICE_CHANNEL_INACTIVE
4340
if self._current_channel is None

actions/Deafen.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ def __init__(self, *args, **kwargs):
2727

2828
def on_ready(self):
2929
super().on_ready()
30-
self.backend.register_callback(
31-
VOICE_SETTINGS_UPDATE, self._update_display)
30+
self.register_backend_callback(VOICE_SETTINGS_UPDATE, self._update_display)
3231

3332
def create_event_assigners(self):
3433
self.event_manager.add_event_assigner(

actions/DiscordCore.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ def __init__(self, *args, **kwargs):
2323
self.color_name: str = ""
2424
self.backend: "Backend" = self.plugin_base.backend
2525

26+
# Track registered callbacks for cleanup
27+
self._registered_callbacks: list[tuple[str, callable]] = []
28+
2629
self.plugin_base.asset_manager.icons.add_listener(self._icon_changed)
2730
self.plugin_base.asset_manager.colors.add_listener(self._color_changed)
2831

@@ -40,6 +43,25 @@ def create_generative_ui(self):
4043
def create_event_assigners(self):
4144
pass
4245

46+
def register_backend_callback(self, key: str, callback: callable):
47+
"""Register a callback and track it for cleanup."""
48+
self.backend.register_callback(key, callback)
49+
self._registered_callbacks.append((key, callback))
50+
51+
def cleanup_callbacks(self):
52+
"""Unregister all tracked callbacks to prevent memory leaks."""
53+
for key, callback in self._registered_callbacks:
54+
self.backend.unregister_callback(key, callback)
55+
self._registered_callbacks.clear()
56+
57+
def __del__(self):
58+
"""Clean up callbacks when action is destroyed."""
59+
try:
60+
self.cleanup_callbacks()
61+
except (AttributeError, RuntimeError):
62+
# Object may be partially initialized or backend already destroyed
63+
pass
64+
4365
def display_icon(self):
4466
if not self.current_icon:
4567
return

actions/Mute.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ def __init__(self, *args, **kwargs):
2727

2828
def on_ready(self):
2929
super().on_ready()
30-
self.backend.register_callback(
31-
VOICE_SETTINGS_UPDATE, self._update_display)
30+
self.register_backend_callback(VOICE_SETTINGS_UPDATE, self._update_display)
3231

3332
def create_event_assigners(self):
3433
self.event_manager.add_event_assigner(

actions/TogglePTT.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ def __init__(self, *args, **kwargs):
3030

3131
def on_ready(self):
3232
super().on_ready()
33-
self.backend.register_callback(
34-
VOICE_SETTINGS_UPDATE, self._update_display)
33+
self.register_backend_callback(VOICE_SETTINGS_UPDATE, self._update_display)
3534

3635
def create_event_assigners(self):
3736
self.event_manager.add_event_assigner(

backend.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,17 @@ def register_callback(self, key: str, callback: callable):
134134
if self._is_authed:
135135
self.discord_client.subscribe(key)
136136

137+
def unregister_callback(self, key: str, callback: callable):
138+
"""Remove a callback from the callback list."""
139+
callbacks = self.callbacks.get(key, [])
140+
if callback in callbacks:
141+
callbacks.remove(callback)
142+
if callbacks:
143+
self.callbacks[key] = callbacks
144+
else:
145+
# Remove key entirely if no callbacks remain
146+
del self.callbacks[key]
147+
137148
def _ensure_connected(self) -> bool:
138149
"""Ensure client is connected, trigger reconnection if needed."""
139150
if self.discord_client is None or not self.discord_client.is_connected():

0 commit comments

Comments
 (0)