Application Architecture
This document provides a comprehensive guide to the architecture of the SDP QA Display application. It details the primary components, data flow patterns, and design principles, serving as an essential resource for developers working on the project.
Architectural Overview
The application’s architecture is designed for clarity, scalability, and maintainability. It is built on a clean separation of concerns, revolving around three core pillars:
Centralized Data Transport: A unified WebSocket client,
DataSocket, manages all real-time data streams, providing a consistent interface for data consumption throughout the application.State Management with React Context: Runtime configuration, such as the selected subarray, beam, or processing block, is managed globally through React Context. This provides a single source of truth and eliminates the need for prop-drilling.
Composable Data Hooks: Custom React Hooks serve as the primary mechanism for components to access and shape live data. These hooks encapsulate subscription logic and data transformations, promoting code reuse and simplifying component implementation.
This structure results in thin, focused components that are easy to test, maintain, and extend.
Application Shell
File: src/App/App.tsx
The main application shell establishes the global layout, theme, and providers.
Layout: Uses
AppWrapperfor a consistent layout structure across all pages.Providers: Wraps the entire application with necessary providers for theme, internationalization (i18n), user authentication (MSAL), and the custom configuration contexts.
Core UI: Renders the main navigation, header (including the Subarray/Beam selectors), and the routing outlet for pages.
Pages and Routing
Pages Folder: src/pages/
Router File: src/App/App.tsx
The application includes a suite of visibility pages, each dedicated to a specific metric visualization.
Visibility Pages
SpectrumPage.tsxSpectrogramPage.tsxPhaseAmplitudePage.tsxBandAvgCorrPage.tsxWeightDistributionPage.tsx
Each page consumes one or more data hooks to get its data and responds to changes from the configuration contexts (e.g., subarray or beam selection).
Router Integration
Routes for all pages are defined in src/App/App.tsx. The visibility pages are mapped as follows:
/vis/spectrum→SpectrumPage/vis/spectrogram→SpectrogramPage/vis/phaseamplitude→PhaseAmplitudePage/vis/bandavgcorr→BandAvgCorrPage/vis/weightdistribution→WeightDistributionPage
Configuration Contexts
Folder: src/services/configWebSocket/
The application uses React’s Context API to manage globally relevant state, such as the currently selected subarray and beam. This state is managed by a central WebSocketManager and exposed to the component tree via React Context providers.
State Management (``configWebSocket.ts``): The
WebSocketManagerclass is a singleton responsible for establishing a WebSocket connection to receive configuration data. It holds the definitive list of available subarrays, beams, and their current state.Context Providers (``subarrayContext.tsx``, ``beamContext.tsx``): These files contain the React components that act as Context Providers. They instantiate or subscribe to the
WebSocketManagerand make the relevant state (e.g., the list of subarrays or the currently selected beam) available to the entire component tree.Updaters (``SubarrayDropDown.tsx``, ``BeamDropDown.tsx``): UI components, such as dropdown menus, are responsible for calling methods on the
WebSocketManagerto update the selected state (e.g.,setSelectedSubarray(newId)).Consumers: Hooks and pages throughout the application consume the context to get the current selection state. This allows them to derive the correct data topics and API queries, and they automatically re-render when the context value changes.
Benefits
Single Source of Truth: Provides a centralized and predictable location for application-wide state.
Decoupling: Avoids prop-drilling and allows components to consume state without being tightly coupled to their parent’s data-fetching logic.
Reactivity: All subscribers automatically re-render when the context value changes.
DataSocket: The Unified WebSocket Client
File: src/services/webSocket/DataSocket.ts
The DataSocket class is the cornerstone of the application’s data transport layer. It abstracts away the complexities of WebSocket communication, providing a robust and generic client for all real-time data needs.
Key Capabilities
Protocol Support: Natively handles both JSON and MsgPack protocols, automatically configuring the
binaryTypebased on the subscription requirements.Generic Payloads: The client is typed with a generic (
DataWebSocket<Payload>), allowing for type-safe data consumption in hooks and components.Status Handling: Emits
SOCKET_STATUSupdates for connection state and automatically recognizes{ status: ... }control frames from the backend.Automatic Reconnection: Implements a bounded exponential backoff strategy for reconnection, ensuring resilience against temporary network or backend outages. Manual disconnections are respected and do not trigger reconnection.
Robust Decoding: Gracefully handles and reports decoding errors, surfacing them through the status and error properties of the data hook.
Local Mocking: Includes a built-in mock data provider that is activated when
DATA_LOCAListrue. This allows developers to work with deterministic, streamed mock payloads fromsrc/mockData/WebSocket/*without needing a live backend connection.
Topic and Metric Mapping
Topics follow the convention
metrics-<metric>-.... TheDataSocketclass extracts the<metric>portion to route to the correct mock data source in local development mode.Metric types are enumerated in
METRIC_TYPES, which are used to manage subscriptions and organize data flow to the appropriate pages.
Key Advantages
Eliminates Boilerplate: Centralizes socket setup, teardown, reconnection, and decoding logic, preventing duplication across the codebase.
Testable and Swappable: The abstraction makes the data transport layer easy to test in isolation and allows for future enhancements without impacting consumer components.
Separation of Concerns: Keeps pages and hooks focused on rendering and data transformation, rather than I/O and connection management.
Data Hooks: Typed Subscriptions and Shaping
Folder: src/hooks/
Data hooks are the standard way for components to subscribe to and consume real-time data. They bridge the gap between the DataSocket service and the UI.
``useDataWebSocket.ts``: This is the generic, low-level subscription hook that wraps the
DataSocketclient. It manages the subscription lifecycle, returning a standard{ data, status, isLoading, topic, messageCount, lastUpdate }object, and ensures the socket connection is properly closed on component unmount.Specialized Hooks: Hooks like
useLagPlotData.tsanduseSpectrogramData.tscomposeuseDataWebSocket. They are responsible for specifying the correct topic and protocol, and performing any necessary data shaping or transformation required by their corresponding UI components.
Advantages of using Hooks
Co-location: Subscription and data-shaping logic are co-located, making it easy to understand how data is fetched and prepared for a component.
Simplified Components: Pages and components are simplified, as they are freed from managing subscription state and lifecycle.
Improved Testability: Hooks can be unit-tested in isolation by mocking the underlying
DataSocketor its responses.
REST Helper for Configuration: getFlows
File: src/services/getFlows/getFlows.ts
The getFlows helper is responsible for fetching flow configuration data for a given processing block.
Purpose and Functionality
Data Retrieval: In a production environment, it performs a
GETrequest to the/config/flowsAPI endpoint, with support for request cancellation via anAbortController.Mocking: In local development mode (
DATA_LOCAL = true), it returns a filtered set of mock data frommockFlowswithout making a network request.Stable Dependencies: It memoizes the list of metrics to include, ensuring a stable function identity to prevent unnecessary re-fetches in
useEffecthooks.Standardized Output: Returns a consistent
{ flowData, isLoading, error, processingBlockID }object for predictable consumption in the UI.
This helper centralizes configuration retrieval logic, ensuring consistency across all pages that require it.
Local vs. Remote Data Modes
The application can run in two data modes, controlled by the DATA_LOCAL flag in @/utils/constants. This is a key feature for developer productivity.
- Local Mode (``DATA_LOCAL = true``):
WebSockets: The
DataSocketclient simulates a connection, streaming mock data from files insrc/mockData/WebSocket. No real socket is opened.REST: Configuration requests are served from on-disk mocks in
src/mockData.
- Remote Mode (``DATA_LOCAL = false``):
WebSockets: The client connects to the live WebSocket endpoint at
${WS_API_URL}/ws/ws.REST: API requests are made to the live backend at
${DATA_API_URL}.
This dual-mode system allows developers to work on features and UI without requiring a live, data-producing backend.
Data Hook Contract and Error Handling
Hook Contract
Any data hook in the application should adhere to a minimal contract:
Inputs: Accepts any optional metric type or data transformation functions.
Outputs: Data hooks typically return one or more objects, such as
{ data, socketStatus, isLoading }for WebSocket subscriptions. Thedataproperty contains the latest payload received from the backend (or mock source), shaped according to the hook’s logic. TheisLoadingproperty is a boolean indicating whether the hook is actively waiting for initial data or a response. ThesocketStatusproperty is a value from theSOCKET_STATUSenum, and other properties may vary depending on the hook’s purpose and the data source.
Error Handling and Edge Cases
The architecture is designed to be resilient and handle common edge cases gracefully:
Null Selections: If a required context value (like
subarrayId) is null, hooks remain idle and do not attempt to open a socket.Protocol Mismatches: An attempt to decode a binary message as text (or vice-versa) will result in a decode error, which is surfaced to the UI via
status=ERRORand theerrorobject.Backend Outages: The
DataSocket’s automatic reconnection logic handles temporary service interruptions. The UI can use thestatusproperty to display the current connection state to the user.Rapid Context Switching: Quickly changing the subarray or beam selection will trigger the old socket to close and a new one to open, without leaking events from the previous subscription.
Developer Guide: Adding a New Metric Page
Follow these steps to add a new page with a live data visualization:
Define Metric: If the metric is new, add its topic/name type to
METRIC_TYPES.Create Data Hook: Create a new, specialized hook in
src/hooks/that composesuseDataWebSocket. Configure it with the correct topic and any required data-shaping logic.Create Page Component: Add a new page component under
src/pages/<Feature>/YourPage.tsx. This component should consume your new hook and any necessary configuration contexts.Register Route: Add a new route for your page in
src/App/App.tsx.Provide Mock Data: For local development, add a mock payload in
src/mockData/WebSocketand update the routing logic inDataSocket.simulateLocalData()to serve it.Add REST Helper (if needed): If the page requires additional configuration from a REST API, create a helper function similar to
getFlows.
Architectural Principles
The architecture is guided by the following principles:
Consistency: A single, standardized way to subscribe to live data reduces cognitive load and makes the system more predictable.
Resilience: Built-in reconnection and clear error surfacing improve runtime stability and the user experience.
Testability: Separating concerns allows hooks, components, and services to be tested in isolation with mocks.
Extensibility: The pattern for adding new metrics and pages is straightforward and repeatable.
Developer Experience: A robust local mocking system ensures that development can proceed efficiently without a dependency on a live backend.
Best Practices and Guidelines
Topic Naming: Keep topic names consistent with the
metrics-<metric>-...convention for predictable mocking and routing.Connection Status UI: Consider adding small UI affordances (e.g., a status badge) that are driven by the
statusproperty from data hooks to give users feedback on the data connection.Binary Payloads: When introducing new binary payloads, ensure the corresponding hook selects the correct protocol and provides the appropriate decoding logic.