Fix memory leak in Alpine.js computed properties

Replace getter-based computed properties with cached properties
and explicit watchers to prevent memory leak in:
- script_requests.php
- script_validation.php
This commit is contained in:
mahdahar 2026-02-15 18:16:16 +07:00
parent c1cf2bbc9f
commit 3577ee870f
2 changed files with 118 additions and 44 deletions

View File

@ -47,6 +47,13 @@ document.addEventListener('alpine:init', () => {
currentPage: 1, currentPage: 1,
pageSize: 30, pageSize: 30,
// Cached computed properties to prevent memory leak
filtered: [],
sorted: [],
paginated: [],
totalPages: 1,
validatedCount: 0,
sort(col) { sort(col) {
if (this.sortCol === col) { if (this.sortCol === col) {
this.sortAsc = !this.sortAsc; this.sortAsc = !this.sortAsc;
@ -64,12 +71,30 @@ document.addEventListener('alpine:init', () => {
if (this.currentPage > 1) this.currentPage--; if (this.currentPage > 1) this.currentPage--;
}, },
get totalPages() { // Compute methods - called only when dependencies change
return Math.ceil(this.filtered.length / this.pageSize) || 1; computeFiltered() {
let filteredList = this.list;
if (this.filterKey === 'Validated') {
filteredList = filteredList.filter(item => this.isValidated(item));
} else {
const validStatuses = this.statusMap[this.filterKey];
if (validStatuses.length > 0) {
filteredList = filteredList.filter(item => validStatuses.includes(item.STATS));
}
}
if (this.filterTable) {
const searchTerm = this.filterTable.toLowerCase();
filteredList = filteredList.filter(item =>
Object.values(item).some(value =>
String(value).toLowerCase().includes(searchTerm)
)
);
}
this.filtered = filteredList;
}, },
get sorted() { computeSorted() {
return this.filtered.slice().sort((a, b) => { this.sorted = this.filtered.slice().sort((a, b) => {
let modifier = this.sortAsc ? 1 : -1; let modifier = this.sortAsc ? 1 : -1;
if (a[this.sortCol] < b[this.sortCol]) return -1 * modifier; if (a[this.sortCol] < b[this.sortCol]) return -1 * modifier;
if (a[this.sortCol] > b[this.sortCol]) return 1 * modifier; if (a[this.sortCol] > b[this.sortCol]) return 1 * modifier;
@ -77,10 +102,14 @@ document.addEventListener('alpine:init', () => {
}); });
}, },
get paginated() { computePaginated() {
const start = (this.currentPage - 1) * this.pageSize; const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize; const end = start + this.pageSize;
return this.sorted.slice(start, end); this.paginated = this.sorted.slice(start, end);
},
computeTotalPages() {
this.totalPages = Math.ceil(this.filtered.length / this.pageSize) || 1;
}, },
init() { init() {
@ -97,6 +126,34 @@ document.addEventListener('alpine:init', () => {
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
this.$watch('list', () => {
this.computeFiltered();
this.computeValidatedCount();
});
this.$watch('filterKey', () => {
this.computeFiltered();
});
this.$watch('filterTable', () => {
this.computeFiltered();
});
this.$watch('filtered', () => {
this.computeSorted();
this.computeTotalPages();
});
this.$watch('sortCol', () => {
this.computeSorted();
});
this.$watch('sortAsc', () => {
this.computeSorted();
});
this.$watch('sorted', () => {
this.computePaginated();
});
this.$watch('currentPage', () => {
this.computePaginated();
});
this.fetchList(); this.fetchList();
}, },
@ -139,28 +196,9 @@ document.addEventListener('alpine:init', () => {
isValidated(item) { isValidated(item) {
return item.ISVAL == 1 && item.ISPENDING != 1; return item.ISVAL == 1 && item.ISPENDING != 1;
}, },
get filtered() {
let filteredList = this.list; computeValidatedCount() {
if (this.filterKey === 'Validated') { this.validatedCount = this.list.filter(r => this.isValidated(r)).length;
filteredList = filteredList.filter(item => this.isValidated(item));
} else {
const validStatuses = this.statusMap[this.filterKey];
if (validStatuses.length > 0) {
filteredList = filteredList.filter(item => validStatuses.includes(item.STATS));
}
}
if (this.filterTable) {
const searchTerm = this.filterTable.toLowerCase();
filteredList = filteredList.filter(item =>
Object.values(item).some(value =>
String(value).toLowerCase().includes(searchTerm)
)
);
}
return filteredList;
},
get validatedCount() {
return this.list.filter(r => this.isValidated(r)).length;
}, },
/* /*

View File

@ -15,6 +15,13 @@ document.addEventListener('alpine:init', () => {
currentPage: 1, currentPage: 1,
pageSize: 30, pageSize: 30,
// Cached computed properties to prevent memory leak
unvalidatedFiltered: [],
unvalidatedSorted: [],
unvalidatedPaginated: [],
unvalidatedTotalPages: 1,
unvalidatedCount: 0,
sort(col) { sort(col) {
if (this.sortCol === col) { if (this.sortCol === col) {
this.sortAsc = !this.sortAsc; this.sortAsc = !this.sortAsc;
@ -32,12 +39,22 @@ document.addEventListener('alpine:init', () => {
if (this.currentPage > 1) this.currentPage--; if (this.currentPage > 1) this.currentPage--;
}, },
get unvalidatedTotalPages() { // Compute methods - called only when dependencies change
return Math.ceil(this.unvalidatedFiltered.length / this.pageSize) || 1; computeUnvalidatedFiltered() {
if (!this.filterTable) {
this.unvalidatedFiltered = this.unvalidatedList;
} else {
const searchTerm = this.filterTable.toLowerCase();
this.unvalidatedFiltered = this.unvalidatedList.filter(item =>
Object.values(item).some(value =>
String(value).toLowerCase().includes(searchTerm)
)
);
}
}, },
get unvalidatedSorted() { computeUnvalidatedSorted() {
return this.unvalidatedFiltered.slice().sort((a, b) => { this.unvalidatedSorted = this.unvalidatedFiltered.slice().sort((a, b) => {
let modifier = this.sortAsc ? 1 : -1; let modifier = this.sortAsc ? 1 : -1;
if (a[this.sortCol] < b[this.sortCol]) return -1 * modifier; if (a[this.sortCol] < b[this.sortCol]) return -1 * modifier;
if (a[this.sortCol] > b[this.sortCol]) return 1 * modifier; if (a[this.sortCol] > b[this.sortCol]) return 1 * modifier;
@ -45,24 +62,18 @@ document.addEventListener('alpine:init', () => {
}); });
}, },
get unvalidatedPaginated() { computeUnvalidatedPaginated() {
const start = (this.currentPage - 1) * this.pageSize; const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize; const end = start + this.pageSize;
return this.unvalidatedSorted.slice(start, end); this.unvalidatedPaginated = this.unvalidatedSorted.slice(start, end);
}, },
get unvalidatedFiltered() { computeUnvalidatedTotalPages() {
if (!this.filterTable) return this.unvalidatedList; this.unvalidatedTotalPages = Math.ceil(this.unvalidatedFiltered.length / this.pageSize) || 1;
const searchTerm = this.filterTable.toLowerCase();
return this.unvalidatedList.filter(item =>
Object.values(item).some(value =>
String(value).toLowerCase().includes(searchTerm)
)
);
}, },
get unvalidatedCount() { computeUnvalidatedCount() {
return this.unvalidatedList.length; this.unvalidatedCount = this.unvalidatedList.length;
}, },
init() { init() {
@ -75,6 +86,31 @@ document.addEventListener('alpine:init', () => {
this.currentPage = 1; this.currentPage = 1;
}); });
// Set up watchers to update cached computed properties
this.$watch('unvalidatedList', () => {
this.computeUnvalidatedFiltered();
this.computeUnvalidatedCount();
});
this.$watch('filterTable', () => {
this.computeUnvalidatedFiltered();
});
this.$watch('unvalidatedFiltered', () => {
this.computeUnvalidatedSorted();
this.computeUnvalidatedTotalPages();
});
this.$watch('sortCol', () => {
this.computeUnvalidatedSorted();
});
this.$watch('sortAsc', () => {
this.computeUnvalidatedSorted();
});
this.$watch('unvalidatedSorted', () => {
this.computeUnvalidatedPaginated();
});
this.$watch('currentPage', () => {
this.computeUnvalidatedPaginated();
});
// Auto-fetch on page load // Auto-fetch on page load
this.fetchUnvalidated(); this.fetchUnvalidated();