From 3fec7684882fc7894a34966f4927eee5f3285f11 Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Mon, 25 May 2026 18:22:07 +0100 Subject: [PATCH] refactor: improve subscription cleanup with token-based pattern Replace object-based subscription tracking with token-based system to prevent memory leaks. Add dispose() method to BaseAIClass and ensure all components properly unsubscribe in onDestroy. Update SettingsService.subscribeToSettingsChanged to return subscription token instead of requiring subscriber object parameter. --- AIClasses/BaseAIClass.ts | 10 ++++++++-- Components/ChatInput.svelte | 7 +++---- Components/ChatModeSelector.svelte | 10 +++++----- Components/UserInstruction.svelte | 5 +++-- Services/DependencyService.ts | 8 +++++++- Services/SettingsService.ts | 8 +++++--- 6 files changed, 31 insertions(+), 17 deletions(-) diff --git a/AIClasses/BaseAIClass.ts b/AIClasses/BaseAIClass.ts index 9b8fb65..934c5da 100644 --- a/AIClasses/BaseAIClass.ts +++ b/AIClasses/BaseAIClass.ts @@ -20,13 +20,15 @@ import { AIToolUsageMode } from "Enums/AIToolUsageMode"; export abstract class BaseAIClass implements IAIClass { protected apiKey: string; - + protected readonly provider: AIProvider; protected readonly abortService: AbortService; protected readonly aiFileService: IAIFileService; protected readonly settingsService: SettingsService; protected readonly streamingService: StreamingService; + private readonly settingsSubscription: object; + private _systemPrompt: string = ""; private _userInstruction: string = ""; private _agentType: AgentType = AgentType.Main; @@ -40,12 +42,16 @@ export abstract class BaseAIClass implements IAIClass { this.settingsService = Resolve(Services.SettingsService); this.streamingService = Resolve(Services.StreamingService); - this.settingsService.subscribeToSettingsChanged(this, () => { + this.settingsSubscription = this.settingsService.subscribeToSettingsChanged(() => { this.apiKey = this.settingsService.getApiKeyForProvider(provider); }); this.apiKey = this.settingsService.getApiKeyForProvider(provider); } + public dispose(): void { + this.settingsService.unsubscribe(this.settingsSubscription); + } + public get currentProvider(): AIProvider { return this.provider; } diff --git a/Components/ChatInput.svelte b/Components/ChatInput.svelte index f0dd40e..4f22a67 100644 --- a/Components/ChatInput.svelte +++ b/Components/ChatInput.svelte @@ -33,8 +33,6 @@ export let onSubmit: (userRequest: string, formattedRequest: string) => void; export let onStop: () => void; - const componentToken = {}; - const inputService: InputService = Resolve(Services.InputService); const settingsService: SettingsService = Resolve(Services.SettingsService); const userInputService: UserInputService = Resolve(Services.UserInputService); @@ -75,14 +73,14 @@ const diffClosedRef: EventRef = eventService.on(Event.DiffClosed, () => { inputMode = InputMode.Normal; focusInput(); }); const rateLimitCountdownRef: EventRef = eventService.on(Event.RateLimitCountdown, (delayMs: number) => { startCountdown(delayMs); }); - settingsService.subscribeToSettingsChanged(componentToken, () => { + const settingsSubscription: object = settingsService.subscribeToSettingsChanged(() => { chatMode = settingsService.settings.chatMode; editsAllowed = chatModeAllowsEdits(chatMode); if (chatModeButton){ setIcon(chatModeButton, iconForChatMode(chatMode)); } }); - + onMount(async () => { userInstructionActive = (await aiPrompt.userInstruction()).trim() !== ""; inputInitialHeight = textareaElement.innerHeight; @@ -92,6 +90,7 @@ eventService.offref(diffOpenedRef); eventService.offref(diffClosedRef); eventService.offref(rateLimitCountdownRef); + settingsService.unsubscribe(settingsSubscription); stopCountdown(); }); diff --git a/Components/ChatModeSelector.svelte b/Components/ChatModeSelector.svelte index 51e4264..d6df2b7 100644 --- a/Components/ChatModeSelector.svelte +++ b/Components/ChatModeSelector.svelte @@ -1,7 +1,7 @@