Fixes #11: Memory management enhancements

- Add clearDomCache() for jQuery object cache invalidation on panel rebuild
- Track all event handlers in on()/once() for complete cleanup by unregisterAllEvents()
- Migrate direct eventSource.on() calls to tracked onEvent() in index.js, injector.js, sillyTavernExpressions.js
- Add clearDebugLogs() called on CHAT_CHANGED to prevent memory accumulation
- Add trackJQueryHandler()/cleanupJQueryEvents() infrastructure for jQuery event cleanup
- Add comprehensive memory management tests (14 new tests, all passing)
This commit is contained in:
2026-07-12 14:31:14 +02:00
parent 3c4e632906
commit 9c0f09619d
6 changed files with 332 additions and 15 deletions
+12
View File
@@ -12,6 +12,12 @@ import { eventSource, event_types } from '../../../../../../script.js';
*/
export function on(eventType, handler) {
eventSource.on(eventType, handler);
// Track for cleanup
if (!registeredHandlers.has(eventType)) {
registeredHandlers.set(eventType, []);
}
registeredHandlers.get(eventType).push(handler);
}
/**
@@ -21,6 +27,12 @@ export function on(eventType, handler) {
*/
export function once(eventType, handler) {
eventSource.once(eventType, handler);
// Track for cleanup (one-time handlers should also be tracked)
if (!registeredHandlers.has(eventType)) {
registeredHandlers.set(eventType, []);
}
registeredHandlers.get(eventType).push(handler);
}
/**
+57
View File
@@ -594,3 +594,60 @@ export function setMusicPlayerContainer($element) {
export function setEquipmentContainer($element) {
$equipmentContainer = $element;
}
/**
* Clears all cached DOM element references.
* Call this when the panel is destroyed or rebuilt to prevent stale jQuery references.
*/
export function clearDomCache() {
$panelContainer = null;
$userStatsContainer = null;
$infoBoxContainer = null;
$thoughtsContainer = null;
$inventoryContainer = null;
$questsContainer = null;
$musicPlayerContainer = null;
$equipmentContainer = null;
}
/**
* Clears the debug logs array.
* Call this on chat change to prevent memory accumulation across sessions.
*/
export function clearDebugLogs() {
debugLogs.length = 0;
}
/**
* Tracks jQuery delegated event handlers for cleanup.
* Each handler is stored with its namespace for targeted unbinding.
*/
const jqueryEventHandlers = [];
/**
* Register a jQuery delegated event handler for tracking.
* @param {string} event - Event type with optional namespace (e.g., 'click.rpgCompanion')
* @param {string} selector - Delegated selector
* @param {Function} handler - Event handler function
* @returns {Function} The same handler for chaining
*/
export function trackJQueryHandler(event, selector, handler) {
jqueryEventHandlers.push({ event, selector, handler });
return handler;
}
/**
* Unbinds all tracked jQuery delegated event handlers.
* Call this when the extension is disabled or the panel is destroyed.
* @param {Function} $ - jQuery function (passed from caller)
*/
export function cleanupJQueryEvents($) {
for (const { event, selector, handler } of jqueryEventHandlers) {
if (handler) {
$(document).off(event, selector, handler);
} else {
$(document).off(event, selector);
}
}
jqueryEventHandlers.length = 0;
}
+6 -4
View File
@@ -4,7 +4,8 @@
*/
import { getContext } from '../../../../../../extensions.js';
import { extension_prompt_types, extension_prompt_roles, setExtensionPrompt, eventSource, event_types } from '../../../../../../../script.js';
import { extension_prompt_types, extension_prompt_roles, setExtensionPrompt, event_types } from '../../../../../../../script.js';
import { on as onEvent } from '../../core/events.js';
import {
extensionSettings,
committedTrackerData,
@@ -892,15 +893,16 @@ ${contextInstructionsText}
export function initHistoryInjectionListeners() {
// Register persistent listeners for prompt injection
// These check pendingContextMap and only inject if there's data
// Use tracked registration (onEvent) so unregisterAllEvents() cleans them up
// Primary: BEFORE_COMBINE for text completion (more reliable - modifies message objects)
eventSource.on(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, onGenerateBeforeCombinePrompts);
onEvent(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, onGenerateBeforeCombinePrompts);
// Fallback: AFTER_COMBINE for text completion (string-based injection)
eventSource.on(event_types.GENERATE_AFTER_COMBINE_PROMPTS, onGenerateAfterCombinePrompts);
onEvent(event_types.GENERATE_AFTER_COMBINE_PROMPTS, onGenerateAfterCombinePrompts);
// Chat completion (OpenAI, etc.)
eventSource.on(event_types.CHAT_COMPLETION_PROMPT_READY, onChatCompletionPromptReady);
onEvent(event_types.CHAT_COMPLETION_PROMPT_READY, onChatCompletionPromptReady);
console.log('[RPG Companion] History injection listeners initialized');
}
+2 -2
View File
@@ -1,7 +1,6 @@
import { Fuse } from '../../../../../../lib.js';
import {
characters,
eventSource,
event_types,
generateQuietPrompt,
generateRaw,
@@ -11,6 +10,7 @@ import {
substituteParamsExtended,
this_chid
} from '../../../../../../script.js';
import { once as onceEvent } from '../core/events.js';
import {
doExtrasFetch,
extension_settings as stExtensionSettings,
@@ -562,7 +562,7 @@ export async function classifyExpressionText(text, { characterName = '' } = {})
}
};
eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onReady);
onceEvent(event_types.TEXT_COMPLETION_SETTINGS_READY, onReady);
const responseText = settings.promptType === PROMPT_TYPE.full
? await generateQuietPrompt({ quietPrompt: prompt })