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, and shared configuration data such as the list of configured flows. 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. Consumers subscribe to a type (and optional id), which sends a { action: "listen", type, id } message to the backend; the manager then caches and re-emits listen_event updates for that topic. It holds the definitive list of available subarrays, beams, their current state, and the list of configured flows.

  • Context Providers (``subarrayContext.tsx``, ``beamContext.tsx``, ``flowContext.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, the currently selected beam, or the list of configured flows) available to the entire component tree. flowContext.tsx subscribes once, application-wide, to the list_flow topic (no id/filter is needed), keeping a live list of ConfiguredFlow entries via useFlows().

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


Flow Configuration: FlowContext

File: src/services/configWebSocket/flowContext.tsx

The list of configured flows (which maps a processing block/metric pair to its data topic) is provided by FlowProvider and consumed via the useFlows() hook.

Purpose and Functionality

  • Push-Based Updates: Rather than polling a REST endpoint whenever the list of processing block IDs changes, FlowProvider subscribes once, on mount, to the list_flow topic on the config WebSocketManager. The backend pushes an updated flow list whenever it changes.

  • Mocking: In local development mode (DATA_LOCAL = true), the WebSocketManager serves the mockFlows fixture for the list_flow topic without making a network request.

  • Single Shared Subscription: Because FlowProvider is mounted once near the root of the application (alongside SubarrayProvider and BeamProvider), all pages share one flow list rather than each hook instance fetching its own copy.

  • Local Filtering: useDataWebSocket.ts filters the shared flow list down to the entry matching its own metric and the current processingBlockIDs (from useSubarray()), memoized so the filtered result is only recomputed when the flow list, processing block IDs, or metric change.

This replaces the previous getFlows REST helper, which issued a GET /config/flows request every time the processing block IDs changed and once per metric-specific hook instance.


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.

    • Config WebSocket: The WebSocketManager serves on-disk mocks from src/mockData (e.g. mockSubarrayDetails, mockFlows) for each subscribed topic instead of connecting to a real socket.

    • REST: Any remaining REST-based configuration requests (e.g. useConfigData) 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.

    • Config WebSocket: The WebSocketManager connects to the live config endpoint at ${CONFIG_WS_URL} and sends listen messages for each subscribed topic (e.g. subarray_state, list_flow).

    • REST: Any remaining REST-based configuration 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 Configuration (if needed): If the page requires additional shared configuration state, expose it via a new (or existing) Context Provider subscribed to a config WebSocket topic, following the pattern used by flowContext.tsx, rather than polling a REST endpoint.


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.