fix: plug memory leaks in blob URLs and traffic cache

- Add URL.revokeObjectURL on unmount and media removal in
  AdminBroadcastCreate, AdminPinnedMessageCreate, Support
- Track blob URLs via refs to revoke before creating new ones
- Add clearCreateAttachment/clearReplyAttachment helpers in Support
  to properly revoke blob URLs when clearing attachments
- Cap adminTraffic cache at 20 entries with FIFO eviction to prevent
  unbounded memory growth from different filter combinations
This commit is contained in:
Fringg
2026-02-23 17:15:11 +03:00
parent 17b2f2e903
commit 7cf72735ec
4 changed files with 85 additions and 13 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useRef } from 'react';
import { useState, useRef, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { motion } from 'framer-motion';
@@ -151,7 +151,7 @@ export default function Support() {
log.debug('Component loaded');
const { t } = useTranslation();
const { isAdmin } = useAuthStore();
const isAdmin = useAuthStore((state) => state.isAdmin);
const queryClient = useQueryClient();
const { openTelegramLink, openLink } = usePlatform();
const [selectedTicket, setSelectedTicket] = useState<TicketDetail | null>(null);
@@ -166,6 +166,34 @@ export default function Support() {
const [replyAttachment, setReplyAttachment] = useState<MediaAttachment | null>(null);
const createFileInputRef = useRef<HTMLInputElement>(null);
const replyFileInputRef = useRef<HTMLInputElement>(null);
const createPreviewRef = useRef<string | null>(null);
const replyPreviewRef = useRef<string | null>(null);
// Revoke blob URLs on unmount to prevent memory leaks
useEffect(() => {
const createRef = createPreviewRef;
const replyRef = replyPreviewRef;
return () => {
if (createRef.current) URL.revokeObjectURL(createRef.current);
if (replyRef.current) URL.revokeObjectURL(replyRef.current);
};
}, []);
const clearCreateAttachment = () => {
if (createPreviewRef.current) {
URL.revokeObjectURL(createPreviewRef.current);
createPreviewRef.current = null;
}
clearCreateAttachment();
};
const clearReplyAttachment = () => {
if (replyPreviewRef.current) {
URL.revokeObjectURL(replyPreviewRef.current);
replyPreviewRef.current = null;
}
clearReplyAttachment();
};
// Get support configuration
const { data: supportConfig, isLoading: configLoading } = useQuery({
@@ -213,8 +241,11 @@ export default function Support() {
return;
}
// Create preview
// Revoke old blob URL before creating new one
const previewRef = setAttachment === setCreateAttachment ? createPreviewRef : replyPreviewRef;
if (previewRef.current) URL.revokeObjectURL(previewRef.current);
const preview = URL.createObjectURL(file);
previewRef.current = preview;
setAttachment({ file, preview, uploading: true });
try {
@@ -250,7 +281,7 @@ export default function Support() {
setShowCreateForm(false);
setNewTitle('');
setNewMessage('');
setCreateAttachment(null);
clearCreateAttachment();
setSelectedTicket(ticket);
},
});
@@ -268,7 +299,7 @@ export default function Support() {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ticket', selectedTicket?.id] });
setReplyMessage('');
setReplyAttachment(null);
clearReplyAttachment();
},
});
@@ -444,7 +475,7 @@ export default function Support() {
onClick={() => {
setShowCreateForm(true);
setSelectedTicket(null);
setCreateAttachment(null);
clearCreateAttachment();
}}
>
<PlusIcon />
@@ -469,7 +500,7 @@ export default function Support() {
onClick={() => {
setSelectedTicket(ticket as unknown as TicketDetail);
setShowCreateForm(false);
setReplyAttachment(null);
clearReplyAttachment();
}}
className={`w-full rounded-bento border p-4 text-left transition-all ${
selectedTicket?.id === ticket.id
@@ -574,7 +605,7 @@ export default function Support() {
{createAttachment ? (
<AttachmentPreview
attachment={createAttachment}
onRemove={() => setCreateAttachment(null)}
onRemove={() => clearCreateAttachment()}
/>
) : (
<button
@@ -608,7 +639,7 @@ export default function Support() {
variant="secondary"
onClick={() => {
setShowCreateForm(false);
setCreateAttachment(null);
clearCreateAttachment();
}}
>
{t('common.cancel')}
@@ -715,7 +746,7 @@ export default function Support() {
{replyAttachment ? (
<AttachmentPreview
attachment={replyAttachment}
onRemove={() => setReplyAttachment(null)}
onRemove={() => clearReplyAttachment()}
/>
) : (
<button