Skip to content

feat(proposal-datasets): live dataset table updates with SSE - #2427

Open
Junjiequan wants to merge 21 commits into
masterfrom
sse-integration
Open

feat(proposal-datasets): live dataset table updates with SSE#2427
Junjiequan wants to merge 21 commits into
masterfrom
sse-integration

Conversation

@Junjiequan

@Junjiequan Junjiequan commented Jun 16, 2026

Copy link
Copy Markdown
Member

Description

Adds real-time dataset updates to the proposal-datasets table via Server-Sent Events (SSE). When enabled, newly created datasets appear in the table automatically and the new row flashes briefly to draw attention, without a manual reload.

Fixes:

###Render fix (key change)
Switched the shared table's trackBy from index-based to _id-based identity tracking with index as fallback, and removed the empty-then-refill (data = []) on reload. Previously every reload remounted all rows, causing the whole table to re-animate. Now a reload that adds one dataset reuses existing row DOM nodes and only mounts the new row, so only it animates.

Changes:

Real-time toggle

Adds a Live updates toggle to the shared DynamicMatTableComponent, gated to logged-in users. The parent owns the SSE connection; the table only renders the toggle and emits its state.

###SSE wiring
EventsService manages the EventSource lifecycle (connect on toggle-on, disconnect on toggle-off and on destroy) and exposes a message$ stream.
On Dataset.created, the proposal-datasets page reloads the list newest-first so the new dataset appears at the top.

Per-row highlight

New rows flash for a few seconds, each on its own timer, so multiple arrivals highlight independently rather than cancelling each other.
Highlight is applied via a separate visual layer (box-shadow overlay) so it no longer conflicts with the existing zebra-stripe background-color.

Tests included

  • Included for each change/fix?
  • Passing? (Merge will not be approved unless this is checked)

Documentation

  • swagger documentation updated [required]
  • official documentation updated [nice-to-have]

official documentation info

If you have updated the official documentation, please provide PR # and URL of the pages where the updates are included

Backend version

  • Does it require a specific version of the backend
  • which version of the backend is required:

Summary by Sourcery

Add optional real-time dataset updates to the proposal datasets table using a shared SSE events service and visual cues for live activity.

New Features:

  • Introduce a shared EventsService that connects to the backend SSE stream using the user token and exposes incoming messages and transient latest-updated IDs.
  • Add an optional live-updates slide toggle and animated border/highlight states to the dynamic material table for indicating real-time activity.
  • Enable live dataset updates in the proposal datasets view, refreshing and highlighting rows when new datasets are created for logged-in users.

Enhancements:

  • Refactor proposal datasets subscriptions to support multiple concurrent subscriptions and ensure proper cleanup on destroy.
  • Wire table components and core directive to accept real-time related inputs and emit toggle changes without impacting existing table behavior.

Summary by Sourcery

Integrate optional real-time dataset updates into the proposal datasets table using a shared SSE events service and visual highlighting for new rows.

New Features:

  • Add an EventsService that manages an authenticated SSE connection and exposes incoming event messages and latest-updated dataset IDs.
  • Expose an optional Live updates toggle and real-time visual states (border and per-row flash) in the shared dynamic material table component.
  • Enable live updates for the proposal datasets view so newly created datasets for the active proposal appear automatically and are highlighted when SSE is enabled.

Enhancements:

  • Refactor proposal datasets subscriptions to support multiple concurrent subscriptions with proper cleanup on destroy.
  • Change table row tracking to use dataset _id instead of index and avoid fully resetting the data source on updates to prevent unnecessary remounting and animations.

Build:

  • Increase the global anyComponentStyle maximumError size limit in angular.json to accommodate the additional table styles.

@Junjiequan
Junjiequan requested a review from Copilot June 23, 2026 11:26
@Junjiequan
Junjiequan marked this pull request as ready for review June 23, 2026 11:26
@Junjiequan
Junjiequan requested a review from a team as a code owner June 23, 2026 11:26
@Junjiequan Junjiequan changed the title Sse integration feat(live-update): proposal-dataset live update table Jun 23, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • EventsService.latestUpdatedId$ assumes every SSE message has a data._id field; consider narrowing this stream to the event types you expect (e.g. Dataset.created) and adding guards so malformed or different message shapes don’t cause runtime errors.
  • Because EventsService is provided in root and connect/disconnect are global, toggling live updates off in one component will tear down the SSE connection for all consumers; consider ref-counting active subscribers or tracking per-feature enablement so one view doesn’t inadvertently disable updates elsewhere.
  • The DynamicMatTableComponent latestUpdatedId @input will receive null/undefined values from the async pipe when there’s no event yet; it would be safer to type this as string | null and short‑circuit in the setter to avoid adding invalid ids to the highlighted set.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- EventsService.latestUpdatedId$ assumes every SSE message has a data._id field; consider narrowing this stream to the event types you expect (e.g. Dataset.created) and adding guards so malformed or different message shapes don’t cause runtime errors.
- Because EventsService is provided in root and connect/disconnect are global, toggling live updates off in one component will tear down the SSE connection for all consumers; consider ref-counting active subscribers or tracking per-feature enablement so one view doesn’t inadvertently disable updates elsewhere.
- The DynamicMatTableComponent latestUpdatedId @Input will receive null/undefined values from the async pipe when there’s no event yet; it would be safer to type this as string | null and short‑circuit in the setter to avoid adding invalid ids to the highlighted set.

## Individual Comments

### Comment 1
<location path="src/app/proposals/proposal-datasets/proposal-datasets.component.ts" line_range="146-153" />
<code_context>

   ngOnInit(): void {
+    this.subscriptions.push(
+      this.eventsService.message$
+        .pipe(
+          filter((payload) => {
+            return payload.type === "Dataset.created";
+          }),
+        )
+        .subscribe((payload: Record<string, any>) => {
+          if (!payload.data.proposalIds.includes(this.proposalId)) return;
+          this.store.dispatch(
+            fetchProposalDatasetsAction({
</code_context>
<issue_to_address>
**issue (bug_risk):** The Dataset.created handler assumes `payload.data.proposalIds` always exists, which can throw for unexpected payloads.

Because only `payload.type` is checked, a malformed or changed event shape (e.g. missing `data` or `data.proposalIds`) will throw at `.includes()`. Please guard access defensively, e.g.:

```ts
const proposalIds = payload.data?.proposalIds as string[] | undefined;
if (!proposalIds?.includes(this.proposalId)) return;
```

This avoids runtime errors from unexpected payloads.
</issue_to_address>

### Comment 2
<location path="src/app/shared/events.service.ts" line_range="23-24" />
<code_context>
+
+  message$ = this.messageSubject.asObservable();
+
+  latestUpdatedId$ = this.messageSubject.pipe(
+    map((m) => (m["data"] as { _id: string })._id),
+  );
+
</code_context>
<issue_to_address>
**issue (bug_risk):** latestUpdatedId$ assumes every event has `data._id`, which may cause runtime errors if other event types are emitted.

If `messageSubject` ever emits an event without `data._id`, this cast `(m["data"] as { _id: string })._id` will throw at runtime. Consider filtering to only the relevant event types before mapping, or making this defensive (e.g. guard checks / optional chaining and handling an undefined ID at the call site).
</issue_to_address>

### Comment 3
<location path="src/app/shared/events.service.ts" line_range="16" />
<code_context>
+} from "rxjs";
+
+@Injectable({ providedIn: "root" })
+export class EventsService {
+  private eventSource: EventSource | null = null;
+  private connectionSub: Subscription | null = null;
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the EventsService so RxJS manages the EventSource lifecycle and domain-specific projections live outside this generic transport service.

You can simplify this service by letting RxJS own the EventSource lifecycle instead of tracking `eventSource` and `open/closeConnection` imperatively, while keeping the `connect`/`disconnect` API and behavior intact.

### 1. Wrap `EventSource` in an observable and remove `eventSource` state

```ts
import { EMPTY, Observable, Subject, Subscription, distinctUntilChanged, switchMap } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class EventsService {
  private connectionSub: Subscription | null = null;
  private messageSubject = new Subject<Record<string, unknown>>();

  message$ = this.messageSubject.asObservable();

  constructor(
    private ngZone: NgZone,
    private store: Store,
  ) {}

  private createEventStream(token: string): Observable<Record<string, unknown>> {
    return new Observable<Record<string, unknown>>((observer) => {
      const es = new EventSource(`/api/v3/events/stream?token=${token}`);

      es.onmessage = (event) => {
        this.ngZone.run(() => observer.next(JSON.parse(event.data)));
      };

      es.onerror = () => {
        es.close();
        observer.complete(); // same effect as closeConnection(): no more events
      };

      return () => es.close();
    });
  }

  connect() {
    if (this.connectionSub) return;

    this.store.dispatch(fetchScicatTokenAction());

    this.connectionSub = this.store.select(selectScicatToken).pipe(
      distinctUntilChanged(),
      switchMap((token) => (token ? this.createEventStream(token) : EMPTY)),
    ).subscribe((msg) => this.messageSubject.next(msg));
  }

  disconnect() {
    this.connectionSub?.unsubscribe();
    this.connectionSub = null;
  }
}
```

This:

- Removes `eventSource`, `openConnection`, and `closeConnection` as mutable state and imperative wiring.
- Keeps the current `connect`/`disconnect` contract and token-driven behavior.
- Centralizes lifecycle in a single Rx pipeline (`selectScicatToken``switchMap``EventSource`).

### 2. Consider moving `latestUpdatedId$` closer to the domain

If possible, keep this service transport-focused and define domain-specific projections where they are used:

```ts
// In feature/domain layer, not in EventsService
const latestUpdatedId$ = eventsService.message$.pipe(
  map((m) => (m['data'] as { _id: string })._id),
);
```

This avoids baking a specific message shape into the generic event service and keeps it simpler to extend when more event types are introduced.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/app/proposals/proposal-datasets/proposal-datasets.component.ts
Comment thread src/app/shared/events.service.ts
Comment thread src/app/shared/events.service.ts
@Junjiequan Junjiequan changed the title feat(live-update): proposal-dataset live update table feat(proposal-datasets): live dataset table updates with SSE Jun 23, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds optional Server-Sent Events (SSE) support to show real-time dataset creations in the proposal datasets table, including a UI toggle, a live-border indicator, and per-row highlight behavior. This is implemented via a new shared EventsService and updates to the shared dynamic material table component.

Changes:

  • Introduces a new EventsService that manages an EventSource connection and exposes message$ / latestUpdatedId$.
  • Extends DynamicMatTableComponent with a “Live updates” toggle, live-border styling, row flash styling, and improves trackBy to prefer _id.
  • Wires ProposalDatasetsComponent to toggle SSE on/off, reload on Dataset.created, and pass the latest-updated id down for row highlighting.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.module.ts Imports MatSlideToggleModule for the new live-updates toggle UI.
src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.ts Adds highlight tracking, latestUpdatedId input handling, and changes trackBy to prefer _id.
src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.scss Adds live-border and row flash animations plus minor layout styling.
src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.html Renders the live-updates toggle and applies row highlight class.
src/app/shared/modules/dynamic-material-table/cores/table.core.directive.ts Adds inputs/outputs for the live-updates toggle state plumbing.
src/app/shared/events.service.ts New singleton service for SSE connection lifecycle and message streams.
src/app/proposals/proposal-datasets/proposal-datasets.component.ts Subscribes to SSE messages, reloads on Dataset.created, toggles SSE connection.
src/app/proposals/proposal-datasets/proposal-datasets.component.html Wires live-updates inputs/outputs and latest-updated id into the shared table.
src/app/datasets/dashboard/dashboard.component.ts Removes an unused import.
angular.json Increases the anyComponentStyle budget maximum error threshold.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/app/shared/events.service.ts
Comment thread src/app/shared/events.service.ts
Comment thread src/app/shared/events.service.ts Outdated
@nitrosx

nitrosx commented Aug 19, 2026

Copy link
Copy Markdown
Member

Code Review Report: sse-integration Branch


Overview

  1. Verify logic correctness: The SSE integration logic is generally sound. The EventsService properly manages EventSource connections, and the ProposalDatasetsComponent correctly filters and handles Dataset.created events for the current proposal.

  2. Check if all edge cases are handled:

    • Token null checks are present in EventsService (switchMap returns EMPTY if no token)
    • Connection state is properly managed with connectionErrorSubject
    • The filter in ProposalDatasetsComponent (line 153) checks if payload.data.proposalIds includes current proposalId before refetching
    • Subscriptions are properly cleaned up in ngOnDestroy
    • Missing: No explicit error handling for JSON.parse failures in event data
  3. Summarize what the code touched by the changes does: Implements real-time updates for dataset tables via Server-Sent Events. When enabled, the system connects to /api/v3/events/stream?token={token}, listens for Dataset.created events, and automatically refreshes the proposal datasets table when new datasets matching the current proposal are created. Visual indicators (row highlighting, animated table border) show active updates and new items.

  4. Assess whether the changes make sense: The implementation is well-designed and follows Angular best practices. The SSE pattern is appropriate for this use case. The visual feedback (highlighting, border animation) enhances UX. Configuration via app config allows disabling the feature.

  5. Identify any unreachable code: No unreachable code identified. The if (this.connectionSub) return; guard in EventsService.connect() prevents duplicate connections.

Commits responsible: 3d151e4 (main SSE feature), caac11c (connection error icon), 54282ae (proposalId matching check), ad84ee1 (highlight effect), 92c5b23 (row highlight), 405568a (CSS max error increase)



Code Changes

File Change Type Description
src/app/shared/events.service.ts New SSE connection management service
src/app/proposals/proposal-datasets/proposal-datasets.component.ts Modified Added SSE event listening and toggle handler
src/app/proposals/proposal-datasets/proposal-datasets.component.html Modified Added real-time toggle inputs/outputs
src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.ts Modified Added highlight logic, border animation, connection error handling
src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.html Modified Added real-time toggle UI and error icon
src/app/shared/modules/dynamic-material-table/table/dynamic-mat-table.component.scss Modified Added row-highlight, live-border animations and styling
src/app/shared/modules/dynamic-material-table/cores/table.core.directive.ts Modified Added Input/Output for real-time toggle
src/app/app-config.service.ts Modified Added realTimeUpdatesEnabled config option
src/app/admin/schema/frontend.config.jsonforms.json Modified Added config schema for realTimeUpdatesEnabled
src/app/datasets/dashboard/dashboard.component.ts Modified Removed unused import
angular.json Modified Increased CSS maximumError from 10kb to 20kb

Assessment: All changes are necessary for the SSE integration feature. The new service and component modifications work together cohesively.

Improvement Needed:

  • Error handling: The JSON.parse(event.data) in EventsService (line 45) lacks try-catch. Malformed JSON would crash the connection.
  • Memory management: The timer(10000).subscribe() in dynamic-mat-table.component.ts (line 327) creates subscriptions that are never unsubscribed, causing memory leaks. Should use take(1) or store subscription for cleanup.
  • Type safety: The payload.data.proposalIds check assumes structure. Should add type guards or validation.
  • Token exposure: The EventSource URL includes the token as a query parameter, which may be logged in server access logs or browser history.

Verdict: The changes are well-architected and implement a useful feature. With the noted improvements (especially error handling and memory management), the implementation would be production-ready.



Security Review

  1. Are there any potential injection vulnerabilities?

    • YES: The token is passed as a URL query parameter in EventsService (line 39: /api/v3/events/stream?token=${token}). This could expose the token in:
      • Server access logs
      • Browser history
      • Referer headers if the SSE endpoint redirects
    • Mitigation: Use Authorization header instead of query parameter if the backend supports it.
  2. Does this code expose any sensitive user data?

    • YES: The sciCat token (access token) is exposed in the URL. Any system logging URLs could capture this token.
    • The payload.data from SSE events may contain sensitive dataset metadata, but this is displayed in the table anyway.
  3. Are there instances of insecure API usage?

    • YES: EventSource with tokens in URL is an anti-pattern. The token should be sent via the withCredentials option or Authorization header.
    • The connection does not implement reconnection backoff logic, making it potentially vulnerable to DoS if the server repeatedly closes connections.
  4. Could this code lead to an authentication bypass?

    • NO: The token is still required and validated server-side. However, token exposure in URLs could allow token theft if logs are compromised.

Commits responsible: 3d151e4 (EventsService creation), cd27713 (lint fix - no security impact), 18bb26b (tokenSub renaming - no security impact)



Test Coverage

Current State:

  • proposal-datasets.component.spec.ts: Exists but does not test SSE functionality
  • EventsService: No unit tests
  • dynamic-mat-table.component.spec.ts: Not checked, but likely doesn't cover new highlight/connection error features

Missing Coverage:

  1. EventsService.connect() - connection establishment
  2. EventsService.disconnect() - cleanup verification
  3. EventsService.createEventStream() - error handling paths
  4. ProposalDatasetsComponent SSE subscription - filtering logic, refetch triggering
  5. DynamicMatTableComponent highlight logic - timer cleanup, Set operations
  6. Connection error states and UI feedback

Suggestions for Improvement:

// EventsService test examples needed
describe('EventsService', () => {
  it('should create EventSource with token', () => { ... });
  it('should emit connection errors', () => { ... });
  it('should disconnect and cleanup', () => { ... });
  it('should handle malformed JSON gracefully', () => { ... });
});

// ProposalDatasetsComponent SSE test
it('should refetch datasets when matching Dataset.created event received', () => { ... });
it('should not refetch for non-matching proposals', () => { ... });

Commits responsible: None - test gaps exist in current branch



Security Examples

Vulnerability Example File:Line
Token in URL new EventSource(\/api/v3/events/stream?token=${token}`)` events.service.ts:39
No JSON parse error handling JSON.parse(event.data) without try-catch events.service.ts:45
Memory leak via timer timer(10000).subscribe(() => { this.highlighted.delete(id); }) without cleanup dynamic-mat-table.component.ts:327


Testing for Security Use Cases

Token Exposure Testing:

  1. Test: Verify token is not logged in URL

    • Method: Check browser dev tools Network tab → SSE request URL
    • Expected: Token should not appear in URL (use headers instead)
    • File: events.service.ts:39
  2. Test: Verify connection cleanup on component destroy

    • Method: Create component, enable SSE, destroy component, verify EventSource is closed
    • Expected: No active EventSource connections after destroy
    • File: events.service.ts:72-75, proposal-datasets.component.ts:325-326

Error Handling Testing:

  1. Test: Malformed JSON in SSE message

    • Method: Mock EventSource to emit invalid JSON
    • Expected: Error should be caught, connection should remain open or reconnect
    • File: events.service.ts:45
  2. Test: Network error during SSE connection

    • Method: Simulate network failure
    • Expected: connectionErrorSubject should emit true, UI should show error icon
    • File: events.service.ts:48-51, dynamic-mat-table.component.html:17-27

Memory Leak Testing:

  1. Test: Multiple highlight timers
    • Method: Trigger multiple Dataset.created events rapidly
    • Expected: Old timers should be cleaned up or only one subscription per ID
    • File: dynamic-mat-table.component.ts:324-329


Summary

Feature Summary: The sse-integration branch implements real-time dataset table updates via Server-Sent Events. Users can toggle live updates, see new rows highlighted, and get visual feedback about connection status.

Critical Issues:

  1. Security: Token passed in URL query parameter exposes credentials in logs and browser history
  2. Memory: Unsubscribed timer in highlight logic causes memory leaks
  3. Reliability: No error handling for malformed JSON in SSE messages

Code Quality: The implementation follows Angular best practices, uses proper RxJS patterns, and provides good UX with visual feedback. The subscription management is generally good but has the noted memory leak.

Recommendations:

  1. High Priority: Move token from URL to Authorization header in EventsService
  2. High Priority: Add try-catch around JSON.parse in EventsService
  3. Medium Priority: Clean up timer subscriptions in DynamicMatTableComponent
  4. Medium Priority: Add comprehensive unit tests for EventsService
  5. Low Priority: Add type guards for event payload structure

Overall Verdict: The feature is well-implemented and useful, but has security and memory management issues that should be addressed before production deployment.


Attribution

Generated by Mistral Vibe
Co-Authored-By: Mistral Vibe vibe@mistral.ai

@nitrosx nitrosx left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please read the AI review section regarding security and let me know your thoughts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants