diff --git a/src/components/panels/GcodefilesPanel.vue b/src/components/panels/GcodefilesPanel.vue index bd903e684..052a61bde 100644 --- a/src/components/panels/GcodefilesPanel.vue +++ b/src/components/panels/GcodefilesPanel.vue @@ -1235,11 +1235,12 @@ export default class GcodefilesPanel extends Mixins(BaseMixin, ControlMixin) { refreshMetadata(data: FileStateGcodefile[]) { const items = data.filter((file) => !file.isDirectory && !file.metadataRequested && !file.metadataPulled) - items.forEach((file: FileStateGcodefile) => { - this.$store.dispatch('files/requestMetadata', { + this.$store.dispatch( + 'files/requestMetadata', + items.map((file: FileStateGcodefile) => ({ filename: 'gcodes' + this.currentPath + '/' + file.filename, - }) - }) + })) + ) } clickRow(item: FileStateGcodefile, force = false) { diff --git a/src/components/panels/Status/Gcodefiles.vue b/src/components/panels/Status/Gcodefiles.vue index b65a32e48..6ddcb7a66 100644 --- a/src/components/panels/Status/Gcodefiles.vue +++ b/src/components/panels/Status/Gcodefiles.vue @@ -343,12 +343,12 @@ export default class StatusPanelGcodefiles extends Mixins(BaseMixin, ControlMixi const requestItems = gcodes.filter( (file: FileStateGcodefile) => !file.metadataRequested && !file.metadataPulled ) - requestItems.forEach((file: FileStateGcodefile) => { - this.$store.dispatch('files/requestMetadata', { + this.$store.dispatch( + 'files/requestMetadata', + requestItems.map((file: FileStateGcodefile) => ({ filename: 'gcodes/' + file.filename, - }) - }) - + })) + ) return gcodes } diff --git a/src/plugins/webSocketClient.ts b/src/plugins/webSocketClient.ts index 2fdb3e0ad..ffaee7b48 100644 --- a/src/plugins/webSocketClient.ts +++ b/src/plugins/webSocketClient.ts @@ -10,6 +10,7 @@ export class WebSocketClient { reconnectInterval = 1000 reconnects = 0 keepAliveTimeout = 1000 + messageId: number = 0 timerId: number | null = null store: Store | null = null waits: Wait[] = [] @@ -25,6 +26,54 @@ export class WebSocketClient { this.url = url } + handleMessage(data: any) { + const wait = this.getWaitById(data.id) + + // report error messages + if (data.error?.message) { + // only report errors, if not disconnected and no init component + if (data.error?.message !== 'Klippy Disconnected' && !wait?.action?.startsWith('server/')) { + window.console.error(`Response Error: ${data.error.message} (${wait?.action ?? 'no action'})`) + } + + if (wait?.id) { + const modulename = wait.action?.split('/')[1] ?? null + + if (modulename && wait.action?.startsWith('server/') && initableServerComponents.includes(modulename)) { + const component = wait.action.replace('server/', '').split('/')[0] + window.console.error(`init server component ${component} failed`) + this.store?.dispatch('server/addFailedInitComponent', component) + this.store?.dispatch('socket/removeInitComponent', `server/${component}/`) + } + + this.removeWaitById(wait.id) + } + + return + } + + // pass it to socket/onMessage, if no wait exists + if (!wait) { + this.store?.dispatch('socket/onMessage', data) + return + } + + // pass result to action + if (wait?.action) { + let result = data.result + if (result === 'ok') result = { result: result } + if (typeof result === 'string') result = { result: result } + + const preload = {} + if (wait.actionPayload) Object.assign(preload, wait.actionPayload) + Object.assign(preload, { requestParams: wait.params }) + Object.assign(preload, result) + this.store?.dispatch(wait.action, preload) + } + + this.removeWaitById(wait.id) + } + async connect() { this.store?.dispatch('socket/setData', { isConnecting: true, @@ -58,55 +107,13 @@ export class WebSocketClient { if (this.store === null) return const data = JSON.parse(msg.data) - const wait = this.getWaitById(data.id) - - // report error messages - if (data.error?.message) { - // only report errors, if not disconnected and no init component - if (data.error?.message !== 'Klippy Disconnected' && !wait?.action?.startsWith('server/')) { - window.console.error(`Response Error: ${data.error.message} (${wait?.action ?? 'no action'})`) - } - - if (wait?.id) { - const modulename = wait.action?.split('/')[1] ?? null - - if ( - modulename && - wait.action?.startsWith('server/') && - initableServerComponents.includes(modulename) - ) { - const component = wait.action.replace('server/', '').split('/')[0] - window.console.error(`init server component ${component} failed`) - this.store?.dispatch('server/addFailedInitComponent', component) - this.store?.dispatch('socket/removeInitComponent', `server/${component}/`) - } - - this.removeWaitById(wait.id) + if (Array.isArray(data)) { + for (const message of data) { + this.handleMessage(message) } - - return + } else { + this.handleMessage(data) } - - // pass it to socket/onMessage, if no wait exists - if (!wait) { - this.store?.dispatch('socket/onMessage', data) - return - } - - // pass result to action - if (wait?.action) { - let result = data.result - if (result === 'ok') result = { result: result } - if (typeof result === 'string') result = { result: result } - - const preload = {} - if (wait.actionPayload) Object.assign(preload, wait.actionPayload) - Object.assign(preload, { requestParams: wait.params }) - Object.assign(preload, result) - this.store?.dispatch(wait.action, preload) - } - - this.removeWaitById(wait.id) } } @@ -130,7 +137,7 @@ export class WebSocketClient { emit(method: string, params: Params, options: emitOptions = {}): void { if (this.instance?.readyState !== WebSocket.OPEN) return - const id = Math.floor(Math.random() * 10000) + 1 + const id = this.messageId++ this.waits.push({ id: id, params: params, @@ -150,6 +157,33 @@ export class WebSocketClient { }) ) } + + emitBatch(messages: BatchMessage[]): void { + if (messages.length === 0) return + if (this.instance?.readyState !== WebSocket.OPEN) return + + const body = [] + for (const { method, params, emitOptions = {} } of messages) { + const id = this.messageId++ + this.waits.push({ + id: id, + params: params, + action: emitOptions.action ?? null, + actionPayload: emitOptions.actionPayload ?? {}, + loading: emitOptions.loading ?? null, + }) + + if (emitOptions.loading) this.store?.dispatch('socket/addLoading', { name: emitOptions.loading }) + body.push({ + jsonrpc: '2.0', + method, + params, + id, + }) + } + + this.instance.send(JSON.stringify(body)) + } } export function WebSocketPlugin(Vue: typeof _Vue, options: WebSocketPluginOptions): void { @@ -169,6 +203,13 @@ export interface WebSocketClient { connect(): void close(): void emit(method: string, params: Params, emitOptions: emitOptions): void + emitBatch(messages: BatchMessage[]): void +} + +export interface BatchMessage { + method: string + params: Params + emitOptions: emitOptions } export interface Wait { diff --git a/src/store/files/actions.ts b/src/store/files/actions.ts index a67e1f114..ca0af88cf 100644 --- a/src/store/files/actions.ts +++ b/src/store/files/actions.ts @@ -11,6 +11,7 @@ import { RootState } from '@/store/types' import i18n from '@/plugins/i18n' import { hiddenDirectories, validGcodeExtensions } from '@/store/variables' import axios from 'axios' +import { BatchMessage } from '@/plugins/webSocketClient' export const actions: ActionTree = { reset({ commit }) { @@ -159,13 +160,26 @@ export const actions: ActionTree = { } }, - requestMetadata({ commit }, payload: { filename: string }) { - const rootPath = payload.filename.slice(0, payload.filename.indexOf('/')) - if (rootPath === 'gcodes') { - const requestFilename = payload.filename.slice(7) - commit('setMetadataRequested', { filename: requestFilename }) - Vue.$socket.emit('server.files.metadata', { filename: requestFilename }, { action: 'files/getMetadata' }) + requestMetadata({ commit }, payload: { filename: string }[]) { + // request file metadata in batches to reduce the number of table re-renders when responses are received + let messages: BatchMessage[] = [] + for (const { filename } of payload) { + if (messages.length >= 100) { + Vue.$socket.emitBatch(messages) + messages = [] + } + const rootPath = filename.slice(0, filename.indexOf('/')) + if (rootPath === 'gcodes') { + const requestFilename = filename.slice(7) + commit('setMetadataRequested', { filename: requestFilename }) + messages.push({ + method: 'server.files.metadata', + params: { filename: requestFilename }, + emitOptions: { action: 'files/getMetadata' }, + }) + } } + Vue.$socket.emitBatch(messages) }, getMetadata({ commit, rootState }, payload) { @@ -203,9 +217,11 @@ export const actions: ActionTree = { payload.item.root === 'gcodes' && validGcodeExtensions.includes(payload.item.path.slice(payload.item.path.lastIndexOf('.'))) ) { - await dispatch('requestMetadata', { - filename: 'gcodes/' + payload.item.path, - }) + await dispatch('requestMetadata', [ + { + filename: 'gcodes/' + payload.item.path, + }, + ]) } break