Skip to content

refactor(frontend): replace RxJS 7 deprecated APIs - #6044

Merged
norman-abramovitz merged 13 commits into
cloudfoundry:developfrom
nabramovitz:norm/refactor/6022-rxjs7-deprecations
Oct 22, 2026
Merged

norman-abramovitz merged 13 commits into
cloudfoundry:developfrom
nabramovitz:norm/refactor/6022-rxjs7-deprecations

Conversation

@nabramovitz

@nabramovitz nabramovitz commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Addresses #6022.

Removes every use of the RxJS APIs that RxJS 7 deprecates and RxJS 8 removes, plus two Angular leftovers that do nothing in this zoneless app: HttpClientModule (duplicating provideHttpClient) and the NgZone wrappers. ESLint rules then reject all of them, so they can't come back. Two user-visible log stream bugs turned up in the code being replaced, and they are fixed here in their own commits.

User-visible fixes

  • The log stream stops after three attempts. The application Log Stream tab limited its retries with take(3) on the retryWhen notifier. The third failure still opened a fourth socket and closed it at once, so the "failed" handler never ran. The count also never reset after a good connection: after fail, fail, connect, fail, the stream ended silently and stopped reconnecting. It now uses retry({ count: maxRetries - 1, delay, resetOnSuccess: true }), which gives three attempts in all, 2 s and 4 s apart, counted afresh after each successful connection.

  • The log stream says when it is retrying or has given up. The log viewer already had messages for both, but since 2019 the status the tab passed it only ever said "connecting" or "connected". The retrying and failed codes went to a subject that nothing read, so after a failed connection the tab said "Connecting to log stream..." for good. The tab now shows:

    • "Connecting to log stream..."
    • "Connection lost. Retrying (attempt 1/2)..." and then "(attempt 2/2)..."
    • "Unable to connect to log stream. Please check that the backend service is running and try refreshing the page."

    The message clears once the stream connects. It sits in a role="status" region so screen readers announce it, and the failure shows in the danger colour.

Replacements

Deprecated Replacement Notes
NgZone run/runOutsideAngular wrappers removed They do nothing in a zoneless app.
HttpClientModule removed It duplicated provideHttpClient.
subscribe(next, error, complete), Observable.create, .observers, throwError(value) observer objects, new Observable, .observed, throwError(() => value) ReposService, which had no callers, is deleted.
multi-argument combineLatest, the combineLatest operator array combineLatest, combineLatestWith The meta card's can check now defaults to of(true) instead of relying on a single-array overload.
toPromise() firstValueFrom defaultValue: undefined where the source may complete without a value.
retryWhen retry({ … }) This is the log stream fix above.
publishReplay(1), refCount() (29 uses) shareReplay See below.

publishReplay(1), refCount() keeps its last value when the subscriber count drops to zero, while shareReplay({ bufferSize: 1, refCount: true }) drops it. That only matters for a source that never completes and whose subscribers can all leave and come back.

  • 28 sites use refCount: true. At each one the source completes after one value, a subscriber keeps the count above zero, or the source replays its current value on subscribe.
  • GlobalEventService is the exception. It is a root service whose page-header banner and events page leave and resubscribe on every navigation, behind a 100 ms debounce. With refCount: true, each new page would show no banner and no unread count for its first 100 ms, so it uses refCount: false. A new spec subscribes, unsubscribes and subscribes again: it passes on develop, fails with refCount: true, and passes with refCount: false.

Lint

The rules are no-restricted-imports and no-restricted-syntax, both at error. They cover the operators above, HttpClientModule, NgZone, .toPromise(), multi-argument subscribe() and combineLatest(), and throwError() without an inline function. typescript-eslint's no-deprecated would need type-aware linting and also reports hundreds of unrelated deprecations, so it is not used here. src/frontend/packages/theme/ is added to the lint paths; it was not linted before and lints clean. Reverse test: putting a deprecated call back gives the expected lint errors.

Also in this PR

  • HelmBaseTestModules, HelmBaseTestProviders and WorkloadsBaseTestingModule, test setup lists with no importers, are deleted.

Tests

  • Log stream retries: new specs replace the web socket with a fake that counts connections. On develop, three failures opened 4 sockets, and the socket opened after fail, fail, connect, fail closed at once. Both pass after the fix.
  • Log stream status: 4 new rendered specs cover connecting and connected, retrying with the attempt number, failed after the limit, and the reset after a reconnect. They failed before the fix (5 failed, 2 passed in the file). Two reverse tests went red: dropping the retry status, and dropping the [status] binding.
  • Branding: new StratosBrandingService specs cover all three config load outcomes. They pass both before and after the firstValueFrom change.
  • GlobalEventService: the resubscribe spec described above.
  • Metrics polling: a range selector spec checks the window poll (6a34127aaa); each of 3 mutations turns it red.
  • make check gate: passed on this branch (4411 tests), and again on develop f88c08f + fix(core): show the Metrics tab for covered endpoints #6050 + this branch (4595 passed, 1 skipped; Go lint 0 issues).

Live checks

Against a Cloud Foundry lab in a browser, since unit tests don't cover these paths. Each failure says whether it was real or faked.

Check Result
XSRF header on a POST and a DELETE Pass
Log stream: connect, give up after three attempts, reconnect Pass, real failures (backend stopped): 3 attempts at 0, 2 and 6 s, then "Unable to connect"; after a reconnect the count starts again
Page-header events banner on navigation Pass, faked 500s (browser route): banner in the same frame as the page header, 8 of 8 navigations
Dashboard side nav Pass: desktop collapse and expand; mobile drawer
Session-expiry prompt Pass, real (short session expiry): prompt after 5 idle minutes, Yes renews the session
Endpoints status pulse Pass
Metrics polling Pass, real Prometheus: first request windowed, then 10 rounds exactly 10 s apart with a moving window; no request in the 25 s after leaving the tab. Run on develop + the Metrics tab fix (#6050) + this branch, since the tab does not open on develop
Cached reload of the Helm catalog Pass: away and back makes no chart request

Release note: changelog.d/0021-rxjs7-deprecations.md.

AI disclosure (RFC-0047): prepared with AI coding assistance; reviewed by the author.

The app runs with provideZonelessChangeDetection(), so NgZone.run()
and NgZone.runOutsideAngular() just call their callback. Remove the 12
calls and the 9 NgZone injections that existed only for them, in the
session-expiry poll, the endpoint health-check interval, the metrics
time-window poll, the Kubernetes summary poll, the blur and focus
directives and the markdown content observer. The metrics poll keeps
its explicit ApplicationRef.tick(). ApplicationTabsBaseComponent
injected NgZone without using it.

Delete code that never did anything:

- core/src/leaveEnterAngularZone.ts: two pass-through scheduler
  helpers with no importers.
- DashboardBaseComponent.redrawSideNav() and the (changedMode)
  binding that called it. It poked _modeChanged on a Material drawer,
  but #sidenav is now a plain <div>, so the property is undefined and
  the method returned at its first check.
- The @ViewChild('sidenav') setter in the same component. It
  subscribed to drawer.closedStart, which a <div>'s ElementRef does
  not have, so the subscription was never made.
HttpClientModule is deprecated. AppModule already configures the HTTP
client with provideHttpClient(withXhr(), withInterceptors([...])),
and that configuration is the one the app uses. The HttpClientModule
imported by AppStoreModule and StratosThemeModule, both loaded into
the same root injector, only appended a second copy of Angular's
XSRF interceptor and the adapter for class-based HTTP_INTERCEPTORS
to the interceptor chain. No class-based interceptor is registered
at the root, so that adapter had nothing to run. The built-in XSRF
interceptor that sends the X-XSRF-TOKEN header still comes from
provideHttpClient.

Also drop it from HelmBaseTestModules and WorkloadsBaseTestingModule,
two exported test arrays that no spec uses.
RxJS 7 deprecates several call forms that RxJS 8 removes. Replace
them with the forms RxJS 7 recommends; behaviour is unchanged.

- subscribe(next, error[, complete]) with positional callbacks becomes
  subscribe({ next, error, complete }) at 7 sites: the EULA page,
  backup and restore of endpoints, the markdown preview, the SSH
  viewer, the kubeconfig certificate check and the Kubernetes
  dashboard configuration.
- throwError(value) becomes throwError(() => value) in the monocular
  ChartsService error handler.
- Observable.create(fn) becomes new Observable(fn) for ChartsService's
  cached-charts branch. It still emits the cached list and does not
  complete, as before.
- EventEmitter.observers.length > 0 becomes EventEmitter.observed in
  CardCfRecentAppsComponent.

Delete the monocular ReposService and its provider entry. Nothing
injects it, and its error handler called error.json(), which an
HttpErrorResponse does not have, so it would have thrown a TypeError
instead of reporting the failure. Its throwError(value) was the other
deprecated call of that form.
RxJS 7 deprecates two forms of combineLatest that RxJS 8 removes:

- the creation function called with each source as its own argument,
  combineLatest(a, b). 32 calls, 9 of them through the
  observableCombineLatest import alias, now pass one array,
  combineLatest([a, b]). The emitted tuple is the same.
- the pipeable operator imported from rxjs/operators. Its 8 calls in
  the log viewer, the services helper, CfRolesService and the build
  tab now use combineLatestWith, which emits the same
  [source, ...others] tuple.

MetaCardComponent built its combineLatest input as
(Observable<boolean> | undefined)[], which only matched the
deprecated overload. Every menu item's `can` is set to of(true)
earlier in the same setter when missing, so map it with
`?? of(true)` to give the array a non-optional element type.
Observable.toPromise() is deprecated in RxJS 7 and removed in RxJS 8.

- StratosBrandingService loads /assets/company-config.json, then
  /assets/config/company.json, then falls back to the built-in
  default. Both requests are HttpClient GETs, which emit once or
  error, so firstValueFrom() resolves and rejects exactly as
  toPromise() did. New specs flush each of the three outcomes
  through HttpTestingController and check the company name that
  results; they passed before the change and still pass.
- LoginPageComponent reads the current auth state once before an
  auto-redirect. auth$ comes from toObservable() and can complete
  without a value if the page is torn down mid-redirect; the code
  already returns early on undefined. firstValueFrom() with
  defaultValue: undefined keeps that, where a bare firstValueFrom()
  would reject with EmptyError. It also takes the first value
  itself, so the take(1) goes.
The application log stream tab reconnects its web socket with
retryWhen(), which RxJS 7 deprecates, and the way it limited the
retries had two faults:

- take(3) sat on the retry notifier, so the third failure still
  produced a retry signal. retryWhen() opened a fourth socket on it
  and then, because the notifier had completed, ended the stream and
  closed that socket at once. The "failed permanently" handler never
  ran.
- take(3) counted retries over the life of the tab. A successful
  connection reset the attempt counter used for the back-off and the
  messages, but not take(3). After two failures, a connection, and
  one more failure, the stream ended silently, still in the
  "retrying" state, and stopped reconnecting.

Use retry({ count: maxRetries - 1, delay, resetOnSuccess: true }):
three attempts in all (the first connection and two retries, 2 s and
4 s apart), counted afresh after each successful connection. The
delay callback reports each retry; the third failure reaches the
catchError, which marks the stream failed. The retry count comes from
retry() itself, so the connectionAttempts field goes.

New specs replace the web socket with a fake that counts connections.
Before this change, three failures opened 4 sockets instead of 3, and
the socket opened after fail, fail, connect, fail was closed at once.

Also convert the existing spec in this file from detectChanges() to
await fixture.whenStable().
publishReplay() and refCount() are deprecated in RxJS 7 and removed in
RxJS 8. All 29 uses were the pair publishReplay(1), refCount().

The replacement is not exactly equivalent. publishReplay(1) keeps its
replay buffer when the subscriber count drops to zero, so a later
subscriber gets the old value at once and the source is subscribed
again. shareReplay({ bufferSize: 1, refCount: true }) drops the
buffer and starts over. The two only differ for a source that has
not completed and whose subscriber count can reach zero and recover.

28 sites use shareReplay({ bufferSize: 1, refCount: true }). At each
one the source completes after one value (take(1) or an HTTP call),
or a subscriber that lives as long as the owning component keeps the
count above zero, or the source replays its current value on
subscribe (toObservable, observeSignal, startWith), so a reset buffer
is refilled at once.

GlobalEventService is the exception. It is a root service; the
page-header banner and the events page subscribe to events$ and
leave on every navigation, and its event stream is debounced by
100 ms. With refCount: true each new page would see no events (no
banner, no unread count) for the first 100 ms. It uses
refCount: false, which keeps the subscription and the last value for
the life of the app. A new spec subscribes, unsubscribes and
subscribes again, and expects the events at once: it passed before
this change, fails with refCount: true and passes with
refCount: false.

A comment in AppDeleteSelectionService that named the publishReplay
caches now says shareReplay.
The previous commits removed every use of the RxJS 7 deprecations
listed below and of NgZone and HttpClientModule. Two ESLint rules,
both at "error", stop them coming back:

- no-restricted-imports: publishReplay, refCount, publish,
  publishLast, publishBehavior, multicast and retryWhen from rxjs or
  rxjs/operators; the combineLatest operator from rxjs/operators;
  HttpClientModule; NgZone.
- no-restricted-syntax: .toPromise(); subscribe() with more than one
  argument; combineLatest() or observableCombineLatest() with more
  than one argument; throwError() whose argument is not an inline
  function.

typescript-eslint's no-deprecated rule would catch these without a
list, but it needs type-aware linting and also reports hundreds of
other deprecations (NoopAnimationsModule, RouterTestingModule and
others) that are outside this change.

The throwError rule cannot tell a function variable from an error
value, so write-with-job.ts now passes its abort error factory as
throwError(() => abortError()) instead of throwError(abortError).
Behaviour is the same.

Add src/frontend/packages/theme/ to the lint paths. It was not linted
before, and lints clean.
The application Log Stream tab never told the user it was retrying or
that it had given up. The log viewer already has messages for both, but
the status the tab passes it only ever said "connecting" and then
"connected". The retrying and failed codes went to a subject and a
signal that nothing read, so after a failed connection the tab said
"Connecting to log stream..." for good.

The subject has been write-only since 2019, when the status passed to
the viewer became a value derived from the socket. The retry and its
codes came later, in 2025, written to that subject.

Pass the subject itself to the viewer, debounced as before so the
"connecting" message does not flash. The tab now shows, in turn:

- "Connecting to log stream..."
- "Connection lost. Retrying (attempt 1/2)..." and "(attempt 2/2)..."
- "Unable to connect to log stream. Please check that the backend
  service is running and try refreshing the page."

and clears the message once connected. The retry count starts again
after each good connection.

The viewer counted retries out of a fixed 5; it now takes the number
as an input, which the tab sets to its own 2. Its message sits in a
role="status" region so screen readers announce it, and errors show in
the danger colour. The unread signal goes.
Add the log stream tab's retrying and failed messages to the RxJS 7
fragment's bug fixes.
HelmBaseTestModules, HelmBaseTestProviders and
WorkloadsBaseTestingModule had no importers anywhere in the frontend.
The only mention was a commented-out line in the Helm release values
tab spec, which goes too, along with the imports only these lists
used. Both files keep their other exports, which specs still import.

@norman-abramovitz norman-abramovitz 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.

LGTM

The metrics range selector polls every 10 seconds when a time window is
chosen, re-requesting the window so the chart keeps moving, and ticks
the app because the poll runs from a plain setInterval. This branch
removed the NgZone wrappers around that interval. The manager's spec
only checked that it could be created, and the selector spec checks a
single tick and the stop on destroy.

Check that every poll sends a new request whose one-hour window has
moved on by the poll interval, and ticks the app each time; and that
choosing a custom range stops the poll. Removing the request, the tick
or the stop each turns one of these red.

@norman-abramovitz norman-abramovitz 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.

LGTM

@nabramovitz
nabramovitz marked this pull request as ready for review October 22, 2026 22:38
@norman-abramovitz
norman-abramovitz merged commit 8c06f2c into cloudfoundry:develop Oct 22, 2026
24 checks passed
@nabramovitz
nabramovitz deleted the norm/refactor/6022-rxjs7-deprecations branch October 22, 2026 22:48
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.

2 participants