Enhance Slicer Integration with Object Update Subscription
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Added subscription to object updates for printers, allowing real-time updates of pending slicer uploads.
- Introduced a reference to prevent redundant updates when new data is received.
- Updated the ApiServerContext to manage object update subscriptions, improving data consistency and responsiveness in the SlicerIntegration component.
This commit is contained in:
Tom Butcher 2026-11-07 22:48:37 +00:00
parent 2baf9cffab
commit 12c5cd83f4
2 changed files with 167 additions and 61 deletions

View File

@ -1,4 +1,4 @@
import { useContext, useEffect, useState } from 'react' import { useContext, useEffect, useRef, useState } from 'react'
import { Divider, Flex, Layout, Typography, Button, Badge, Modal } from 'antd' import { Divider, Flex, Layout, Typography, Button, Badge, Modal } from 'antd'
import { LoadingOutlined } from '@ant-design/icons' import { LoadingOutlined } from '@ant-design/icons'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
@ -18,7 +18,9 @@ const { Title } = Typography
const SlicerIntegration = () => { const SlicerIntegration = () => {
const location = useLocation() const location = useLocation()
const printerId = new URLSearchParams(location.search).get('printerId') const printerId = new URLSearchParams(location.search).get('printerId')
const { updateObject } = useContext(ApiServerContext) const { updateObject, subscribeToObjectUpdates } =
useContext(ApiServerContext)
const receivedPendingUploadsUpdateRef = useRef(false)
const [pendingSlicerUploads, setPendingSlicerUploads] = useState([]) const [pendingSlicerUploads, setPendingSlicerUploads] = useState([])
const [currentPendingSlicerUpload, setCurrentPendingSlicerUpload] = const [currentPendingSlicerUpload, setCurrentPendingSlicerUpload] =
@ -43,11 +45,40 @@ const SlicerIntegration = () => {
const [showSlicerUploadFlow, setShowSlicerUploadFlow] = useState(false) const [showSlicerUploadFlow, setShowSlicerUploadFlow] = useState(false)
useEffect(() => { useEffect(() => {
if (receivedPendingUploadsUpdateRef.current) {
return
}
setPendingSlicerUploads( setPendingSlicerUploads(
objectFormState.objectData?.pendingSlicerUploads || [] objectFormState.objectData?.pendingSlicerUploads || []
) )
}, [objectFormState.objectData?.pendingSlicerUploads]) }, [objectFormState.objectData?.pendingSlicerUploads])
useEffect(() => {
if (!printerId) {
return undefined
}
const unsubscribe = subscribeToObjectUpdates(
String(printerId).toLowerCase(),
'printer',
(object) => {
if (
!object ||
!Object.prototype.hasOwnProperty.call(object, 'pendingSlicerUploads')
) {
return
}
receivedPendingUploadsUpdateRef.current = true
setPendingSlicerUploads(object.pendingSlicerUploads || [])
}
)
return () => {
if (unsubscribe) unsubscribe()
}
}, [printerId, subscribeToObjectUpdates])
useEffect(() => { useEffect(() => {
const nextUpload = pendingSlicerUploads.find( const nextUpload = pendingSlicerUploads.find(
(upload) => upload.new === true (upload) => upload.new === true

View File

@ -396,6 +396,8 @@ const ApiServerProvider = ({ children }) => {
const [userSettingsLoaded, setUserSettingsLoaded] = useState(false) const [userSettingsLoaded, setUserSettingsLoaded] = useState(false)
const subscribedCallbacksRef = useRef(new Map()) const subscribedCallbacksRef = useRef(new Map())
const subscribedActivityCallbacksRef = useRef(new Map()) const subscribedActivityCallbacksRef = useRef(new Map())
const subscribedObjectUpdateCallbacksRef = useRef(new Map())
const subscribedObjectUpdateServerRef = useRef(new Set())
const subscribedActivityServerSubscriptionsRef = useRef(new Set()) const subscribedActivityServerSubscriptionsRef = useRef(new Set())
const notificationListenersRef = useRef(new Set()) const notificationListenersRef = useRef(new Set())
const [connectionIssue, setConnectionIssue] = useState(false) const [connectionIssue, setConnectionIssue] = useState(false)
@ -445,6 +447,7 @@ const ApiServerProvider = ({ children }) => {
const clearSubscriptions = useCallback(() => { const clearSubscriptions = useCallback(() => {
subscribedCallbacksRef.current.clear() subscribedCallbacksRef.current.clear()
subscribedActivityServerSubscriptionsRef.current.clear() subscribedActivityServerSubscriptionsRef.current.clear()
subscribedObjectUpdateServerRef.current.clear()
notificationListenersRef.current.clear() notificationListenersRef.current.clear()
}, []) }, [])
@ -505,6 +508,45 @@ const ApiServerProvider = ({ children }) => {
[] []
) )
const resubscribeObjectUpdateListeners = useCallback(
(socket = socketRef.current) => {
if (!socket?.connected) {
return
}
subscribedObjectUpdateCallbacksRef.current.forEach(
(callbacks, callbacksRefKey) => {
if (!callbacks?.length) {
return
}
if (subscribedObjectUpdateServerRef.current.has(callbacksRefKey)) {
return
}
const colonIndex = callbacksRefKey.indexOf(':')
if (colonIndex === -1) {
return
}
const objectType = callbacksRefKey.slice(0, colonIndex)
const objectId = callbacksRefKey.slice(colonIndex + 1)
subscribedObjectUpdateServerRef.current.add(callbacksRefKey)
socket.emit(
'subscribeToObjectUpdate',
{ _id: objectId, objectType },
(result) => {
if (!result?.success) {
subscribedObjectUpdateServerRef.current.delete(callbacksRefKey)
}
}
)
}
)
},
[]
)
const registerNotificationListener = useCallback((callback) => { const registerNotificationListener = useCallback((callback) => {
notificationListenersRef.current.add(callback) notificationListenersRef.current.add(callback)
return () => notificationListenersRef.current.delete(callback) return () => notificationListenersRef.current.delete(callback)
@ -729,6 +771,7 @@ const ApiServerProvider = ({ children }) => {
setConnectionIssue(false) setConnectionIssue(false)
resetReconnectState() resetReconnectState()
resubscribeActivityListeners(newSocket) resubscribeActivityListeners(newSocket)
resubscribeObjectUpdateListeners(newSocket)
}) })
}) })
@ -797,7 +840,8 @@ const ApiServerProvider = ({ children }) => {
getUserSettings, getUserSettings,
resubscribeActivityListeners, resubscribeActivityListeners,
resetReconnectState, resetReconnectState,
markConnectionLost markConnectionLost,
resubscribeObjectUpdateListeners
]) ])
connectToServerRef.current = connectToServer connectToServerRef.current = connectToServer
@ -861,6 +905,10 @@ const ApiServerProvider = ({ children }) => {
subscribedActivityServerSubscriptionsRef.current.clear() subscribedActivityServerSubscriptionsRef.current.clear()
resubscribeActivityListeners(socket) resubscribeActivityListeners(socket)
} }
if (socket && subscribedObjectUpdateCallbacksRef.current.size > 0) {
subscribedObjectUpdateServerRef.current.clear()
resubscribeObjectUpdateListeners(socket)
}
} }
const handleWindowFocus = () => { const handleWindowFocus = () => {
@ -874,6 +922,7 @@ const ApiServerProvider = ({ children }) => {
logger.debug('Page restored from bfcache, reconnecting socket...') logger.debug('Page restored from bfcache, reconnecting socket...')
subscribedActivityServerSubscriptionsRef.current.clear() subscribedActivityServerSubscriptionsRef.current.clear()
subscribedObjectUpdateServerRef.current.clear()
setConnected(false) setConnected(false)
teardownSocket() teardownSocket()
connectToServer() connectToServer()
@ -893,6 +942,7 @@ const ApiServerProvider = ({ children }) => {
authenticated, authenticated,
connectToServer, connectToServer,
resubscribeActivityListeners, resubscribeActivityListeners,
resubscribeObjectUpdateListeners,
attemptReconnect, attemptReconnect,
teardownSocket teardownSocket
]) ])
@ -950,7 +1000,21 @@ const ApiServerProvider = ({ children }) => {
const callbacksRefKey = `${objectType}:${id}` const callbacksRefKey = `${objectType}:${id}`
const allCallbacksRefKey = getAllObjectUpdatesSubscriptionKey(objectType) const allCallbacksRefKey = getAllObjectUpdatesSubscriptionKey(objectType)
if ( const objectUpdateCallbacks =
subscribedObjectUpdateCallbacksRef.current.get(callbacksRefKey)
if (id && objectType && objectUpdateCallbacks?.length) {
logger.debug(
`Calling ${objectUpdateCallbacks.length} callbacks for object:`,
callbacksRefKey
)
objectUpdateCallbacks.forEach((callback) => {
try {
callback(data.object)
} catch (error) {
logger.error('Error in object update callback:', error)
}
})
} else if (
id && id &&
objectType && objectType &&
subscribedCallbacksRef.current.has(callbacksRefKey) subscribedCallbacksRef.current.has(callbacksRefKey)
@ -1106,28 +1170,31 @@ const ApiServerProvider = ({ children }) => {
} }
const offObjectUpdatesEvent = useCallback((id, objectType, callback) => { const offObjectUpdatesEvent = useCallback((id, objectType, callback) => {
if (socketRef.current && socketRef.current.connected == true) { const callbacksRefKey = `${objectType}:${id}`
const callbacksRefKey = `${objectType}:${id}` if (!subscribedObjectUpdateCallbacksRef.current.has(callbacksRefKey)) {
// Remove callback from the subscribed callbacks map return
if (subscribedCallbacksRef.current.has(callbacksRefKey)) { }
const callbacks = subscribedCallbacksRef.current
.get(callbacksRefKey) const callbacks = subscribedObjectUpdateCallbacksRef.current
.filter((cb) => cb !== callback) .get(callbacksRefKey)
if (callbacks.length === 0) { .filter((cb) => cb !== callback)
logger.debug(
'No callbacks found for object:', if (callbacks.length === 0) {
callbacksRefKey, subscribedObjectUpdateCallbacksRef.current.delete(callbacksRefKey)
'unsubscribing from object update...' subscribedObjectUpdateServerRef.current.delete(callbacksRefKey)
) if (socketRef.current?.connected) {
subscribedCallbacksRef.current.delete(callbacksRefKey) logger.debug(
socketRef.current.emit('unsubscribeObjectUpdate', { 'No callbacks found for object:',
_id: id, callbacksRefKey,
objectType: objectType 'unsubscribing from object update...'
}) )
} else { socketRef.current.emit('unsubscribeObjectUpdate', {
subscribedCallbacksRef.current.set(callbacksRefKey, callbacks) _id: id,
} objectType
})
} }
} else {
subscribedObjectUpdateCallbacksRef.current.set(callbacksRefKey, callbacks)
} }
}, []) }, [])
@ -1181,43 +1248,51 @@ const ApiServerProvider = ({ children }) => {
const subscribeToObjectUpdates = useCallback( const subscribeToObjectUpdates = useCallback(
(id, objectType, callback) => { (id, objectType, callback) => {
if (socketRef.current && socketRef.current.connected == true) { if (!id || !objectType || typeof callback !== 'function') {
const callbacksRefKey = `${objectType}:${id}` return undefined
// Add callback to the subscribed callbacks map immediately
if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
subscribedCallbacksRef.current.set(callbacksRefKey, [])
}
const callbacksLength =
subscribedCallbacksRef.current.get(callbacksRefKey).length
if (callbacksLength <= 0) {
socketRef.current.emit(
'subscribeToObjectUpdate',
{
_id: id,
objectType: objectType
},
(result) => {
if (result.success) {
logger.info('Subscribed to id:', id, 'objectType:', objectType)
}
}
)
}
logger.info(
'Adding callback id:',
id,
'objectType:',
objectType,
'callbacks length:',
callbacksLength + 1
)
subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)
// Return cleanup function
return () => offObjectUpdatesEvent(id, objectType, callback)
} }
const callbacksRefKey = `${objectType}:${id}`
if (!subscribedObjectUpdateCallbacksRef.current.has(callbacksRefKey)) {
subscribedObjectUpdateCallbacksRef.current.set(callbacksRefKey, [])
}
const callbacks =
subscribedObjectUpdateCallbacksRef.current.get(callbacksRefKey)
callbacks.push(callback)
const socket = socketRef.current
if (
socket?.connected &&
!subscribedObjectUpdateServerRef.current.has(callbacksRefKey)
) {
subscribedObjectUpdateServerRef.current.add(callbacksRefKey)
socket.emit(
'subscribeToObjectUpdate',
{
_id: id,
objectType
},
(result) => {
if (result?.success) {
logger.info('Subscribed to id:', id, 'objectType:', objectType)
} else {
subscribedObjectUpdateServerRef.current.delete(callbacksRefKey)
}
}
)
}
logger.info(
'Adding callback id:',
id,
'objectType:',
objectType,
'callbacks length:',
callbacks.length
)
return () => offObjectUpdatesEvent(id, objectType, callback)
}, },
[offObjectUpdatesEvent] [offObjectUpdatesEvent]
) )