fix: additional memory leak prevention in Alpine.js watchers and audit events

- Add toastTimeouts array to track and clear pending timeouts
- Consolidate duplicate watchers (sortCol, sortAsc, sorted, currentPage)
- Add destroy() method to clean up data structures and timeouts
- Cache audit events in _cachedAuditEvents instead of computed getter
- Clear item data when closing dialogs
This commit is contained in:
mahdahar 2026-02-16 07:17:09 +07:00
parent 3577ee870f
commit 79e6ab63a0

View File

@ -112,51 +112,69 @@ document.addEventListener('alpine:init', () => {
this.totalPages = Math.ceil(this.filtered.length / this.pageSize) || 1; this.totalPages = Math.ceil(this.filtered.length / this.pageSize) || 1;
}, },
// Track timeouts for cleanup
toastTimeouts: [],
init() { init() {
this.today = new Date().toISOString().slice(0, 10); this.today = new Date().toISOString().slice(0, 10);
this.filter.date1 = this.today; this.filter.date1 = this.today;
this.filter.date2 = this.today; this.filter.date2 = this.today;
this.$watch('filterTable', () => {
this.currentPage = 1;
});
const defaultPrinter = '<?= $config[session()->get("userrole")]["sampleDialog"]["defaultPrinter"] ?? "lab" ?>'; const defaultPrinter = '<?= $config[session()->get("userrole")]["sampleDialog"]["defaultPrinter"] ?? "lab" ?>';
this.selectedPrinter = defaultPrinter || 'lab'; this.selectedPrinter = defaultPrinter || 'lab';
// Set up watchers to update cached computed properties // Single consolidated watcher for list changes
this.$watch('list', () => { this.$watch('list', () => {
this.computeFiltered(); this.computeFiltered();
this.computeValidatedCount(); this.computeValidatedCount();
}); });
this.$watch('filterKey', () => {
this.computeFiltered(); // Single watcher for filter changes
}); this.$watch('filterKey', () => this.computeFiltered());
this.$watch('filterTable', () => { this.$watch('filterTable', () => {
this.currentPage = 1;
this.computeFiltered(); this.computeFiltered();
}); });
// Single watcher for computed chain
this.$watch('filtered', () => { this.$watch('filtered', () => {
this.computeSorted(); this.computeSorted();
this.computeTotalPages(); this.computeTotalPages();
}); });
this.$watch('sortCol', () => {
this.computeSorted(); // Sort changes trigger pagination update
}); this.$watch('sortCol', () => this.computeSorted());
this.$watch('sortAsc', () => { this.$watch('sortAsc', () => this.computeSorted());
this.computeSorted(); this.$watch('sorted', () => this.computePaginated());
}); this.$watch('currentPage', () => this.computePaginated());
this.$watch('sorted', () => {
this.computePaginated(); // Watch audit data changes to recompute events
}); this.$watch('auditData', () => {
this.$watch('currentPage', () => { this.computeAuditEvents();
this.computePaginated();
}); });
// Register cleanup on destroy
this.$cleanup(() => this.destroy());
this.fetchList(); this.fetchList();
}, },
// Cleanup method to prevent memory leaks
destroy() {
// Clear all tracked timeouts
this.toastTimeouts.forEach(id => clearTimeout(id));
this.toastTimeouts = [];
// Clear large data structures
this.list = [];
this.filtered = [];
this.sorted = [];
this.paginated = [];
this.auditData = null;
this.item = null;
this.previewItem = null;
},
fetchList() { fetchList() {
this.isLoading = true; this.isLoading = true;
this.list = []; this.list = [];
@ -215,6 +233,7 @@ document.addEventListener('alpine:init', () => {
closeSampleDialog() { closeSampleDialog() {
this.isDialogSampleOpen = false; this.isDialogSampleOpen = false;
this.item = null;
}, },
fetchItem(accessnumber) { fetchItem(accessnumber) {
@ -282,9 +301,12 @@ document.addEventListener('alpine:init', () => {
auditData: null, auditData: null,
auditAccessnumber: null, auditAccessnumber: null,
auditTab: 'all', auditTab: 'all',
// Cached audit events to prevent memory leak
_cachedAuditEvents: [],
openAuditDialog(accessnumber) { openAuditDialog(accessnumber) {
this.auditAccessnumber = accessnumber; this.auditAccessnumber = accessnumber;
this.auditData = null; this.auditData = null;
this._cachedAuditEvents = [];
this.auditTab = 'all'; this.auditTab = 'all';
this.isDialogAuditOpen = true; this.isDialogAuditOpen = true;
this.fetchAudit(accessnumber); this.fetchAudit(accessnumber);
@ -292,6 +314,7 @@ document.addEventListener('alpine:init', () => {
closeAuditDialog() { closeAuditDialog() {
this.isDialogAuditOpen = false; this.isDialogAuditOpen = false;
this.auditData = null; this.auditData = null;
this._cachedAuditEvents = [];
this.auditAccessnumber = null; this.auditAccessnumber = null;
}, },
fetchAudit(accessnumber) { fetchAudit(accessnumber) {
@ -300,10 +323,14 @@ document.addEventListener('alpine:init', () => {
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}).then(res => res.json()).then(data => { }).then(res => res.json()).then(data => {
this.auditData = data.data; this.auditData = data.data;
this.computeAuditEvents();
}); });
}, },
get getAllAuditEvents() { computeAuditEvents() {
if (!this.auditData) return []; if (!this.auditData) {
this._cachedAuditEvents = [];
return;
}
let events = []; let events = [];
let id = 0; let id = 0;
@ -367,15 +394,18 @@ document.addEventListener('alpine:init', () => {
}); });
}); });
return events.sort((a, b) => { this._cachedAuditEvents = events.sort((a, b) => {
if (!a.datetime) return 1; if (!a.datetime) return 1;
if (!b.datetime) return -1; if (!b.datetime) return -1;
return new Date(a.datetime) - new Date(b.datetime); return new Date(a.datetime) - new Date(b.datetime);
}); });
}, },
get getAllAuditEvents() {
return this._cachedAuditEvents;
},
get getFilteredAuditEvents() { get getFilteredAuditEvents() {
if (this.auditTab === 'all') return this.getAllAuditEvents; if (this.auditTab === 'all') return this._cachedAuditEvents;
return this.getAllAuditEvents.filter(e => e.category === this.auditTab); return this._cachedAuditEvents.filter(e => e.category === this.auditTab);
}, },
@ -410,9 +440,6 @@ document.addEventListener('alpine:init', () => {
} }
}, },
selectedPrinter: 'lab',
printAllLabels(accessnumber) { printAllLabels(accessnumber) {
const printer = this.selectedPrinter || 'lab'; const printer = this.selectedPrinter || 'lab';
fetch(`${BASEURL}/label/all/${accessnumber}/${printer}`, { method: 'GET' }) fetch(`${BASEURL}/label/all/${accessnumber}/${printer}`, { method: 'GET' })
@ -530,7 +557,13 @@ document.addEventListener('alpine:init', () => {
toast.className = `alert alert-${type} fixed top-4 right-4 z-50`; toast.className = `alert alert-${type} fixed top-4 right-4 z-50`;
toast.innerHTML = `<i class="fa ${type === 'error' ? 'fa-times-circle' : 'fa-check-circle'}"></i> ${message}`; toast.innerHTML = `<i class="fa ${type === 'error' ? 'fa-times-circle' : 'fa-check-circle'}"></i> ${message}`;
document.body.appendChild(toast); document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2000); const timeoutId = setTimeout(() => {
toast.remove();
// Remove from tracking array
const idx = this.toastTimeouts.indexOf(timeoutId);
if (idx > -1) this.toastTimeouts.splice(idx, 1);
}, 2000);
this.toastTimeouts.push(timeoutId);
}, },
})); }));
}); });