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:

  1. Centralized Data Transport: A unified WebSocket client, DataSocket, manages all real-time data streams, providing a consistent interface for data consumption throughout the application.

  2. 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.

  3. 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 AppWrapper for 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.tsx

  • SpectrogramPage.tsx

  • PhaseAmplitudePage.tsx

  • BandAvgCorrPage.tsx

  • WeightDistributionPage.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/spectrumSpectrumPage

  • /vis/spectrogramSpectrogramPage

  • /vis/phaseamplitudePhaseAmplitudePage

  • /vis/bandavgcorrBandAvgCorrPage

  • /vis/weightdistributionWeightDistributionPage


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 WebSocketManager class 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 WebSocketManager and 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 WebSocketManager to 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 binaryType based 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_STATUS updates 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_LOCAL is true. This allows developers to work with deterministic, streamed mock payloads from src/mockData/WebSocket/* without needing a live backend connection.

Topic and Metric Mapping

  • Topics follow the convention metrics-<metric>-.... The DataSocket class 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 DataSocket client. 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.ts and useSpectrogramData.ts compose useDataWebSocket. 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 DataSocket or 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 GET request to the /config/flows API endpoint, with support for request cancellation via an AbortController.

  • Mocking: In local development mode (DATA_LOCAL = true), it returns a filtered set of mock data from mockFlows without 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 useEffect hooks.

  • 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 DataSocket client simulates a connection, streaming mock data from files in src/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. The data property contains the latest payload received from the backend (or mock source), shaped according to the hook’s logic. The isLoading property is a boolean indicating whether the hook is actively waiting for initial data or a response. The socketStatus property is a value from the SOCKET_STATUS enum, 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=ERROR and the error object.

  • Backend Outages: The DataSocket’s automatic reconnection logic handles temporary service interruptions. The UI can use the status property 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:

  1. Define Metric: If the metric is new, add its topic/name type to METRIC_TYPES.

  2. Create Data Hook: Create a new, specialized hook in src/hooks/ that composes useDataWebSocket. Configure it with the correct topic and any required data-shaping logic.

  3. 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.

  4. Register Route: Add a new route for your page in src/App/App.tsx.

  5. Provide Mock Data: For local development, add a mock payload in src/mockData/WebSocket and update the routing logic in DataSocket.simulateLocalData() to serve it.

  6. 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 status property 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.