From c6bcff8ecf6c3ebd0e71bb5be6ce2736b1f79401 Mon Sep 17 00:00:00 2001 From: Mathis Mensing Date: Tue, 16 Jan 2024 19:49:16 +0100 Subject: [PATCH] feat(spoolman): live update support (#1301) Signed-off-by: Mathis Mensing Co-authored-by: Pedro Lamas --- .../widgets/spoolman/SpoolmanCard.vue | 4 - src/store/spoolman/actions.ts | 142 +++++++++++++++++- src/store/spoolman/mutations.ts | 6 +- src/store/spoolman/types.ts | 23 +++ 4 files changed, 168 insertions(+), 7 deletions(-) diff --git a/src/components/widgets/spoolman/SpoolmanCard.vue b/src/components/widgets/spoolman/SpoolmanCard.vue index bb6b3bf7b5..861633efdc 100644 --- a/src/components/widgets/spoolman/SpoolmanCard.vue +++ b/src/components/widgets/spoolman/SpoolmanCard.vue @@ -132,10 +132,6 @@ export default class SpoolmanCard extends Mixins(StateMixin) { this.$store.commit('spoolman/setDialogState', { show: true }) } - get supportsSpoolman () { - return this.$store.getters['spoolman/getSupported'] - } - get activeSpool (): Spool | null { return this.$store.getters['spoolman/getActiveSpool'] } diff --git a/src/store/spoolman/actions.ts b/src/store/spoolman/actions.ts index bf614a2348..0ad952a975 100644 --- a/src/store/spoolman/actions.ts +++ b/src/store/spoolman/actions.ts @@ -1,7 +1,17 @@ import type { ActionTree } from 'vuex' -import type { SpoolmanState } from './types' +import type { + Spool, + SpoolmanState, + WebsocketBasePayload, + WebsocketFilamentPayload, + WebsocketSpoolPayload, + WebsocketVendorPayload +} from './types' import type { RootState } from '../types' import { SocketActions } from '@/api/socketActions' +import { consola } from 'consola' + +const logPrefix = '[SPOOLMAN]' export const actions: ActionTree = { /** @@ -23,7 +33,135 @@ export const actions: ActionTree = { commit('setActiveSpool', payload.spool_id) }, - async onAvailableSpools ({ commit }, payload) { + async onSpoolChange ({ commit, getters }, { type, payload }: WebsocketSpoolPayload) { + const spools = [...getters.getAvailableSpools as Spool[]] + + switch (type) { + case 'added': { + spools.push(payload) + + break + } + + case 'updated': { + const index = spools.findIndex(spool => spool.id === payload.id) + + if (index >= 0) { + spools[index] = payload + } + + break + } + + case 'deleted': { + const index = spools.findIndex(spool => spool.id === payload.id) + + if (index >= 0) { + spools.splice(index, 1) + } + + break + } + } + + commit('setAvailableSpools', spools) + }, + + async onFilamentChange ({ commit, getters }, { type, payload }: WebsocketFilamentPayload) { + if (type !== 'updated') { + // we only care about updated filament types + return + } + + const spools = [...getters.getAvailableSpools] + for (const spool of spools) { + if (spool.filament.id === payload.id) { + spools[spools.indexOf(spool)] = { + ...spool, + filament: payload + } + } + } + + commit('setAvailableSpools', spools) + }, + + async onVendorChange ({ commit, getters }, { type, payload }: WebsocketVendorPayload) { + if (type !== 'updated') { + // we only care about updated vendors + return + } + + const spools = [...getters.getAvailableSpools] + for (const spool of spools) { + if (spool.filament.vendor?.id === payload.id) { + spools[spools.indexOf(spool)] = { + ...spool, + filament: { + ...spool.filament, + vendor: payload + } + } + } + } + + commit('setAvailableSpools', spools) + }, + + async onAvailableSpools ({ commit, getters, dispatch }, payload) { commit('setAvailableSpools', [...payload]) + if (getters.getSupported) { dispatch('initializeWebsocketConnection') } + }, + + async initializeWebsocketConnection ({ state, rootState, dispatch }) { + if (rootState.server.config.spoolman?.server) { + if (state.socket?.readyState === WebSocket.OPEN) { + // we already have a working WS conn + return + } + + // init websocket to listen for updates + const spoolmanUrl = new URL(rootState.server.config.spoolman.server) + spoolmanUrl.pathname += `${spoolmanUrl.pathname.endsWith('/') ? '' : '/'}api/v1/` + if (spoolmanUrl.protocol === 'https:') { + spoolmanUrl.protocol = 'wss:' + } else { + spoolmanUrl.protocol = 'ws:' + } + + state.socket = new WebSocket(spoolmanUrl) + state.socket.onerror = err => consola.warn(`${logPrefix} received websocket error`, err) + state.socket.onmessage = event => { + let data + try { + data = JSON.parse(event.data) as WebsocketBasePayload + } catch (err) { + consola.error(`${logPrefix} failed to decode websocket message`, err, event.data) + return + } + + consola.debug(`${logPrefix} received spoolman message:`, data) + switch (data.resource) { + case 'spool': + dispatch('onSpoolChange', data) + break + + case 'filament': + dispatch('onFilamentChange', data) + break + + case 'vendor': + dispatch('onVendorChange', data) + break + + default: + consola.warn(`${logPrefix} ignoring websocket message with type ${data.resource}`) + } + } + } else { + // destroy ws + state.socket?.close() + state.socket = undefined + } } } diff --git a/src/store/spoolman/mutations.ts b/src/store/spoolman/mutations.ts index f549604b91..b5ce80d28c 100644 --- a/src/store/spoolman/mutations.ts +++ b/src/store/spoolman/mutations.ts @@ -1,6 +1,10 @@ import type { MutationTree } from 'vuex' import { defaultState } from './state' -import type { Spool, SpoolmanState, SpoolSelectionDialogState } from '@/store/spoolman/types' +import type { + Spool, + SpoolmanState, + SpoolSelectionDialogState +} from '@/store/spoolman/types' export const mutations: MutationTree = { /** diff --git a/src/store/spoolman/types.ts b/src/store/spoolman/types.ts index 5bead25815..22478a9407 100644 --- a/src/store/spoolman/types.ts +++ b/src/store/spoolman/types.ts @@ -46,9 +46,32 @@ export interface SpoolmanState { activeSpool?: number; supported: boolean; dialog: SpoolSelectionDialogState; + socket?: WebSocket; } export interface SpoolSelectionDialogState { show: boolean; filename?: string; } + +export interface WebsocketBasePayload { + type: 'added' | 'updated' | 'deleted'; + resource: string; + date: string; + payload: Record; +} + +export interface WebsocketSpoolPayload extends WebsocketBasePayload { + resource: 'spool'; + payload: Spool; +} + +export interface WebsocketFilamentPayload extends WebsocketBasePayload { + resource: 'filament'; + payload: Filament; +} + +export interface WebsocketVendorPayload extends WebsocketBasePayload { + resource: 'vendor'; + payload: Vendor; +}