// src/contexts/ApiServerContext.js import React, { createContext, useEffect, useState, useContext, useRef, useCallback } from 'react' import io from 'socket.io-client' import { message, notification, Modal, Space, Button } from 'antd' import PropTypes from 'prop-types' import { AuthContext } from './AuthContext' import axios from 'axios' import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon' import ReloadIcon from '../../Icons/ReloadIcon' import config from '../../../config' import loglevel from 'loglevel' const logger = loglevel.getLogger('ApiServerContext') logger.setLevel(config.logLevel) const ApiServerContext = createContext() const ApiServerProvider = ({ children }) => { const { token, userProfile, authenticated, setUnauthenticated } = useContext(AuthContext) const socketRef = useRef(null) const [connected, setConnected] = useState(false) const [connecting, setConnecting] = useState(false) const [error, setError] = useState(null) const [messageApi, contextHolder] = message.useMessage() const [notificationApi] = notification.useNotification() const [fetchLoading, setFetchLoading] = useState(false) const [showErrorModal, setShowErrorModal] = useState(false) const [errorModalContent, setErrorModalContent] = useState('') const [retryCallback, setRetryCallback] = useState(null) const subscribedCallbacksRef = useRef(new Map()) const subscribedLockCallbacksRef = useRef(new Map()) const notifyLockUpdate = useCallback( async (lockData) => { logger.debug('Notifying lock update:', lockData) const objectId = lockData._id || lockData.id if ( objectId && subscribedLockCallbacksRef.current.has(objectId) && lockData.user != userProfile?._id ) { const callbacks = subscribedLockCallbacksRef.current.get(objectId) logger.debug( `Calling ${callbacks.length} lock callbacks for object:`, objectId ) callbacks.forEach((callback) => { try { callback(lockData) } catch (error) { logger.error('Error in lock update callback:', error) } }) } else { logger.debug( `No lock callbacks found for object: ${objectId}, subscribed lock callbacks:`, Array.from(subscribedLockCallbacksRef.current.keys()) ) } }, [userProfile?._id] ) const connectToServer = useCallback(() => { if (token && authenticated == true) { logger.debug('Token is available, connecting to api server...') const newSocket = io(config.apiServerUrl, { reconnectionAttempts: 3, timeout: 3000, auth: { type: 'user' } }) setConnecting(true) newSocket.on('connect', () => { logger.debug('Api Server connected') newSocket.emit('authenticate', { token: token }, (result) => { console.log('Auth result', result) setConnecting(false) setConnected(true) setError(null) }) }) newSocket.on('objectUpdate', handleObjectUpdate) newSocket.on('objectNew', handleObjectNew) newSocket.on('notify_lock_update', notifyLockUpdate) newSocket.on('disconnect', () => { logger.debug('Api Server disconnected') setError('Api Server disconnected') setConnected(false) }) newSocket.on('connect_error', (err) => { logger.error('Api Server connection error:', err) messageApi.error('Api Server connection error: ' + err.message) setError('Api Server connection error') setConnected(false) }) newSocket.on('bridge.notification', (data) => { notificationApi[data.type]({ title: data.title, message: data.message }) }) newSocket.on('error', (err) => { logger.error('Api Server error:', err) setError('Api Server error') }) socketRef.current = newSocket } }, [token, authenticated, messageApi, notificationApi, notifyLockUpdate]) useEffect(() => { if (token && authenticated == true) { connectToServer() } else if (!token && socketRef.current) { logger.debug('Token not available, disconnecting api server...') socketRef.current.disconnect() socketRef.current = null } // Clean up function return () => { if (socketRef.current) { logger.debug('Cleaning up api server connection...') socketRef.current.disconnect() socketRef.current = null } } }, [token, authenticated, connectToServer]) const lockObject = (id, type) => { logger.debug('Locking ' + id) if (socketRef.current && socketRef.current.connected) { socketRef.current.emit('lock', { _id: id, type: type }) logger.debug('Sent lock command for object:', id) } } const unlockObject = (id, type) => { logger.debug('Unlocking ' + id) if (socketRef.current && socketRef.current.connected == true) { socketRef.current.emit('unlock', { _id: id, type: type }) logger.debug('Sent unlock command for object:', id) } } const fetchObjectLock = async (id, type) => { if (socketRef.current && socketRef.current.connected == true) { logger.debug('Fetching lock status for ' + id) return new Promise((resolve) => { socketRef.current.emit( 'getLock', { _id: id, type: type }, (lockEvent) => { logger.debug('Received lock status for object:', id, lockEvent) if (lockEvent.user != userProfile?._id) { resolve(lockEvent) } else { resolve(null) } } ) logger.debug('Sent fetch lock command for object:', id) }) } } const handleObjectUpdate = async (data) => { logger.debug('Notifying object update:', data) const id = data._id const objectType = data.objectType const callbacksRefKey = `${objectType}:${id}` if ( id && objectType && subscribedCallbacksRef.current.has(callbacksRefKey) ) { const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey) logger.debug( `Calling ${callbacks.length} callbacks for object:`, callbacksRefKey ) callbacks.forEach((callback) => { try { callback(data.object) } catch (error) { logger.error('Error in object update callback:', error) } }) } else { logger.debug( `No callbacks found for object: ${callbacksRefKey}, subscribed callbacks:`, Array.from(subscribedCallbacksRef.current.keys()) ) } } const handleObjectNew = async (data) => { logger.debug('Notifying object new:', data) const objectType = data.objectType || 'unknown' if (objectType && subscribedCallbacksRef.current.has(objectType)) { const callbacks = subscribedCallbacksRef.current.get(objectType) logger.debug( `Calling ${callbacks.length} callbacks for type:`, objectType ) callbacks.forEach((callback) => { try { callback(data.object) } catch (error) { logger.error('Error in object new callback:', error) } }) } else { logger.debug( `No callbacks found for object: ${objectType}, subscribed callbacks:`, Array.from(subscribedCallbacksRef.current.keys()) ) } } const offObjectUpdatesEvent = useCallback((id, objectType, callback) => { if (socketRef.current && socketRef.current.connected == true) { // Remove callback from the subscribed callbacks map if (subscribedCallbacksRef.current.has(id)) { const callbacks = subscribedCallbacksRef.current .get(id) .filter((cb) => cb !== callback) if (callbacks.length === 0) { subscribedCallbacksRef.current.delete(id) socketRef.current.emit('unsubscribe', { id: id, type: objectType }) } else { subscribedCallbacksRef.current.set(id, callbacks) } } } }, []) const offObjectTypeUpdatesEvent = useCallback((objectType, callback) => { if (socketRef.current && socketRef.current.connected == true) { // Remove callback from the subscribed callbacks map if (subscribedCallbacksRef.current.has(objectType)) { const callbacks = subscribedCallbacksRef.current .get(objectType) .filter((cb) => cb !== callback) if (callbacks.length === 0) { subscribedCallbacksRef.current.delete(objectType) socketRef.current.emit('unsubscribe', { objectType: objectType }) } else { subscribedCallbacksRef.current.set(objectType, callbacks) } } } }, []) const subscribeToObjectUpdates = useCallback( (id, objectType, callback) => { if (socketRef.current && socketRef.current.connected == true) { const callbacksRefKey = `${objectType}:${id}` // Add callback to the subscribed callbacks map immediately if (!subscribedCallbacksRef.current.has(callbacksRefKey)) { subscribedCallbacksRef.current.set(callbacksRefKey, []) } subscribedCallbacksRef.current.get(callbacksRefKey).push(callback) socketRef.current.emit( 'subscribeToObjectUpdate', { _id: id, objectType: objectType }, (result) => { if (result.success) { logger.info('Subscribed to id:', id, 'objectType:', objectType) } } ) // Return cleanup function return () => offObjectUpdatesEvent(id, objectType, callback) } }, [offObjectUpdatesEvent] ) const subscribeToObjectTypeUpdates = useCallback( (objectType, callback) => { logger.debug('Subscribing to type updates:', objectType) if (socketRef.current && socketRef.current.connected == true) { // Add callback to the subscribed callbacks map immediately if (!subscribedCallbacksRef.current.has(objectType)) { subscribedCallbacksRef.current.set(objectType, []) } subscribedCallbacksRef.current.get(objectType).push(callback) logger.debug( `Added callback for type ${objectType}, total callbacks: ${subscribedCallbacksRef.current.get(objectType).length}` ) socketRef.current.emit( 'subscribeToObjectTypeUpdate', { objectType: objectType }, (result) => { if (result.success) { logger.info('Subscribed to objectType:', objectType) } } ) logger.debug('Registered update event listener for object:', objectType) // Return cleanup function return () => offObjectTypeUpdatesEvent(objectType, callback) } }, [offObjectTypeUpdatesEvent] ) const offLockEvent = useCallback((id, callback) => { if (socketRef.current && socketRef.current.connected == true) { // Remove callback from the subscribed lock callbacks map if (subscribedLockCallbacksRef.current.has(id)) { const callbacks = subscribedLockCallbacksRef.current .get(id) .filter((cb) => cb !== callback) if (callbacks.length === 0) { subscribedLockCallbacksRef.current.delete(id) } else { subscribedLockCallbacksRef.current.set(id, callbacks) } } logger.debug('Removed lock event listener for object:', id) } }, []) const subscribeToObjectLock = useCallback( (id, type, callback) => { logger.debug('Subscribing to lock for object:', id, 'type:', type) if (socketRef.current && socketRef.current.connected == true) { // Add callback to the subscribed lock callbacks map immediately if (!subscribedLockCallbacksRef.current.has(id)) { subscribedLockCallbacksRef.current.set(id, []) } subscribedLockCallbacksRef.current.get(id).push(callback) logger.debug( `Added lock callback for object ${id}, total lock callbacks: ${subscribedLockCallbacksRef.current.get(id).length}` ) socketRef.current.emit('subscribe_lock', { id: id, type: type }) logger.debug('Registered lock event listener for object:', id) // Return cleanup function return () => offLockEvent(id, callback) } }, [offLockEvent] ) const showError = (error, callback = null) => { const code = error.response.data.code || 'UNKNOWN' if (code == 'UNAUTHORIZED') { setUnauthenticated() return } var content = `Error ${error.code} (${error.status}): ${error.message}` if (error.response?.data?.error) { content = `${error.response?.data?.error} (${error.status})` } setErrorModalContent(content) setRetryCallback(() => callback) setShowErrorModal(true) } const handleRetry = () => { setShowErrorModal(false) setErrorModalContent('') if (retryCallback) { retryCallback() } setRetryCallback(null) } // Generalized fetchObject function const fetchObject = async (id, type) => { const fetchUrl = `${config.backendUrl}/${type}s/${id}` setFetchLoading(true) logger.debug('Fetching from ' + fetchUrl) try { const response = await axios.get(fetchUrl, { headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } }) setFetchLoading(false) return response.data } catch (err) { showError(err, () => { fetchObject(id, type) }) return {} } } // Fetch table data with pagination, filtering, and sorting const fetchObjects = async (type, params = {}) => { const { page = 1, limit = 25, filter = {}, sorter = {}, onDataChange } = params logger.debug('Fetching table data from:', type, { page, limit, filter, sorter }) try { const response = await axios.get( `${config.backendUrl}/${type.toLowerCase()}s`, { params: { page, limit, ...filter, sort: sorter.field, order: sorter.order }, headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } } ) const newData = response.data const totalCount = parseInt(response.headers['x-total-count'] || '0', 10) const totalPages = Math.ceil(totalCount / limit) const hasMore = newData.length >= limit if (onDataChange) { onDataChange(newData) } return { data: newData, totalCount, totalPages, hasMore, page } } catch (err) { showError(err, () => { fetchObjects(type, params) }) return [] } } // Fetch table data with pagination, filtering, and sorting const fetchObjectsByProperty = async (type, params = {}) => { const { filter = {}, properties = [], masterFilter = {} } = params logger.debug('Fetching property object data from:', type, { properties, filter }) try { const response = await axios.get( `${config.backendUrl}/${type.toLowerCase()}s/properties`, { params: { ...filter, properties: properties.join(','), // Convert array to comma-separated string masterFilter: JSON.stringify(masterFilter) }, headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } } ) const newData = response.data return newData } catch (err) { showError(err, () => { fetchObjects(type, params) }) return [] } } // Update filament information const updateObject = async (id, type, value) => { const updateUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}` logger.debug('Updating info for ' + id) try { const response = await axios.put(updateUrl, value, { headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } }) logger.debug('Object updated successfully') if (socketRef.current && socketRef.current.connected == true) { await socketRef.current.emit('update', { _id: id, type: type, updatedAt: response.data.updatedAt }) } return response.data } catch (err) { setError(err, () => { updateObject(id, type, value) }) return {} } } // Update filament information const deleteObject = async (id, type) => { const deleteUrl = `${config.backendUrl}/${type.toLowerCase()}s/${id}` logger.debug('Deleting object ID: ' + id) try { const response = await axios.delete(deleteUrl, { headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } }) logger.debug('Object deleted successfully') if (socketRef.current && socketRef.current.connected == true) { await socketRef.current.emit('update', { _id: id, type: type, updatedAt: response.data.updatedAt }) } return response.data } catch (err) { showError(err, () => { deleteObject(id, type) }) return {} } } // Update filament information const createObject = async (type, value) => { const createUrl = `${config.backendUrl}/${type.toLowerCase()}s` logger.debug('Creating object...') try { const response = await axios.post(createUrl, value, { headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } }) return response.data } catch (err) { showError(err, () => { createObject(type, value) }) return {} } } // Download GCode file content const fetchObjectContent = async (id, type, fileName) => { try { const response = await axios.get( `${config.backendUrl}/${type.toLowerCase()}s/${id}/content`, { headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } } ) const fileURL = window.URL.createObjectURL(new Blob([response.data])) const fileLink = document.createElement('a') fileLink.href = fileURL fileLink.setAttribute('download', fileName) document.body.appendChild(fileLink) fileLink.click() fileLink.parentNode.removeChild(fileLink) } catch (err) { showError(err, () => { fetchObjectContent(id, type, fileName) }) } } // Fetch notes for a specific parent const fetchNotes = async (parentId) => { logger.debug('Fetching notes for parent:', parentId) try { const response = await axios.get(`${config.backendUrl}/notes`, { params: { parent: parentId, sort: 'createdAt', order: 'ascend' }, headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } }) const notesData = response.data || [] logger.debug('Fetched notes:', notesData.length) return notesData } catch (err) { showError(err, () => { fetchNotes(parentId) }) } } const fetchSpotlightData = async (query) => { logger.debug('Fetching spotlight data with query:', query) try { const response = await axios.get( `${config.backendUrl}/spotlight/${query}`, { headers: { Accept: 'application/json', Authorization: `Bearer ${token}` } } ) return response.data } catch (err) { showError(err, () => { fetchSpotlightData(query) }) } } const fetchTemplatePreview = async ( id, content, testObject, scale, callback ) => { logger.debug('Fetching preview...') if (socketRef.current && socketRef.current.connected) { return socketRef.current.emit( 'previewTemplate', { _id: id, content: content, testObject: testObject, scale: scale }, callback ) } } const fetchHostOTP = async (id, callback) => { logger.debug('Fetching host OTP...') if (socketRef.current && socketRef.current.connected) { return socketRef.current.emit( 'generateHostOtp', { _id: id }, callback ) } } const sendObjectAction = async (id, objectType, action, callback) => { logger.debug('Sending object action...') if (socketRef.current && socketRef.current.connected) { return socketRef.current.emit( 'objectAction', { _id: id, objectType: objectType, action: action }, callback ) } } return ( {contextHolder} {children} Error } open={showErrorModal} okText='OK' style={{ maxWidth: 430 }} closable={false} centered maskClosable={true} footer={[ , ]} > {errorModalContent} ) } ApiServerProvider.propTypes = { children: PropTypes.node.isRequired } export { ApiServerContext, ApiServerProvider }