From e699246d87a28f38dae2b1bb756a8c6778f45cca Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Willms <tleilax+studip@gmail.com> Date: Wed, 3 Jul 2024 12:23:52 +0000 Subject: [PATCH] limit number of parallel requests and cache gained info, fixes #4358 Closes #4358 Merge request studip/studip!3161 --- .../javascripts/lib/chunked-requester.ts | 76 +++++++++++++++++++ .../vue/components/tree/StudipTreeList.vue | 2 +- .../vue/components/tree/StudipTreeTable.vue | 2 +- .../components/tree/TreeNodeCourseInfo.vue | 12 +-- resources/vue/mixins/TreeMixin.js | 23 +++++- 5 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 resources/assets/javascripts/lib/chunked-requester.ts diff --git a/resources/assets/javascripts/lib/chunked-requester.ts b/resources/assets/javascripts/lib/chunked-requester.ts new file mode 100644 index 00000000000..6f53c010fa9 --- /dev/null +++ b/resources/assets/javascripts/lib/chunked-requester.ts @@ -0,0 +1,76 @@ +import axios from "axios"; + +interface ChunkedRequest { + url: string, + parameters: object, + resolve(value: any): any, + reject(): any, +} + +export default class ChunkedRequester +{ + #requests: ChunkedRequest[] = []; + + readonly #delay: number; + readonly #limit: number; + #timeout: any = null; + + constructor(limit: number = 16, delay: number = 500) { + if (limit < 1) { + throw new Error('Limit must be positive'); + } + + this.#limit = limit; + this.#delay = delay; + } + + addRequest(url: string, parameters: object = {}): Promise<any> + { + return new Promise((resolve, reject) => { + this.#requests.push({ + url, + parameters, + resolve, + reject + }); + this.#startRequests(); + }); + } + + #startRequests(): void + { + if (this.#requests.length === 0) { + return; + } + + if (this.#requests.length < this.#limit) { + this.clearTimeout(); + } + + if (this.#timeout !== null) { + return; + } + + this.#timeout = setTimeout( + () => { + Promise.all( + this.#requests + .splice(0, this.#limit) + .map(({url, parameters, resolve, reject}) => { + return axios.get(url, {params: parameters}).then(resolve, reject); + }) + ).then(() => { + this.clearTimeout(); + this.#startRequests(); + }); + } + , this.#delay + ); + } + + clearTimeout(): void + { + clearTimeout(this.#timeout); + this.#timeout = null; + } +} diff --git a/resources/vue/components/tree/StudipTreeList.vue b/resources/vue/components/tree/StudipTreeList.vue index 6214234c3e9..155503bd947 100644 --- a/resources/vue/components/tree/StudipTreeList.vue +++ b/resources/vue/components/tree/StudipTreeList.vue @@ -222,7 +222,7 @@ export default { courses: [], assistiveLive: '', subLevelsCourses: 0, - thisLevelCourses: 0, + thisLevelCourses: this.getCachedNodeCourseInfo(this.node.id, this.semester, this.semClass), showingAllCourses: false } }, diff --git a/resources/vue/components/tree/StudipTreeTable.vue b/resources/vue/components/tree/StudipTreeTable.vue index 1dc45a0166e..17030250242 100644 --- a/resources/vue/components/tree/StudipTreeTable.vue +++ b/resources/vue/components/tree/StudipTreeTable.vue @@ -245,7 +245,7 @@ export default { courses: [], assistiveLive: '', subLevelsCourses: 0, - thisLevelCourses: 0, + thisLevelCourses: this.getCachedNodeCourseInfo(this.node.id, this.semester, this.semClass), showingAllCourses: false } }, diff --git a/resources/vue/components/tree/TreeNodeCourseInfo.vue b/resources/vue/components/tree/TreeNodeCourseInfo.vue index 3f3777c9338..859429e7652 100644 --- a/resources/vue/components/tree/TreeNodeCourseInfo.vue +++ b/resources/vue/components/tree/TreeNodeCourseInfo.vue @@ -32,22 +32,24 @@ export default { }, data() { return { - isLoading: false, - courseCount: 0, + courseCount: this.getCachedNodeCourseInfo(this.node, this.semester, this.semClass), showingAllCourses: false } }, + computed: { + isLoading() { + return this.courseCount === null; + } + }, methods: { showAllCourses(state) { this.showingAllCourses = state; this.$emit('showAllCourses', state); }, loadNodeInfo(node) { - this.isLoading = true; this.getNodeCourseInfo(node, this.semester, this.semClass) .then(info => { - this.courseCount = info?.data.courses; - this.isLoading = false; + this.courseCount = info?.data.courses ?? 0; }); } }, diff --git a/resources/vue/mixins/TreeMixin.js b/resources/vue/mixins/TreeMixin.js index 4263eaf938d..1269ffa51c0 100644 --- a/resources/vue/mixins/TreeMixin.js +++ b/resources/vue/mixins/TreeMixin.js @@ -1,4 +1,9 @@ import axios from 'axios'; +import ChunkedRequester from '@/assets/javascripts/lib/chunked-requester'; +import Cache from '@/assets/javascripts/lib/cache'; + +const requester = new ChunkedRequester(); +const cache = Cache.getInstance('tree-info/'); export const TreeMixin = { data() { @@ -56,7 +61,10 @@ export const TreeMixin = { {params: parameters} ); }, - async getNodeCourseInfo(node, semesterId, semClass = 0) { + getCachedNodeCourseInfo(node, semesterId, semClass) { + return cache.get(['course-info', node.id, semesterId, semClass].join('/')) ?? null; + }, + getNodeCourseInfo(node, semesterId, semClass = 0) { let parameters = {}; if (semesterId !== 'all' && semesterId !== '0') { @@ -67,10 +75,17 @@ export const TreeMixin = { parameters['filter[semclass]'] = semClass; } - return axios.get( + return requester.addRequest( STUDIP.URLHelper.getURL('jsonapi.php/v1/tree-node/' + node.id + '/courseinfo'), - { params: parameters } - ); + parameters + ).then(courseinfo => { + cache.set( + ['course-info', node.id, semesterId, semClass].join('/'), + courseinfo.data.courses ?? 0, + 3 * 60 * 60 + ); + return courseinfo; + }); }, nodeUrl(node_id, semester = null ) { return STUDIP.URLHelper.getURL('', { node_id, semester }) -- GitLab